Commits

Robert Brewer committed 8e00505

Upgraded accounts chart to link to transactions

Comments (0)

Files changed (4)

flowrate/__init__.py

             months = range(1, 13)
 
         if dategroup == 'year':
-            dategroups = [datetime.date(y, 12, 31) for y in years]
+            balance_dates = [datetime.date(y, 12, 31) for y in years]
+            dategroups = [str(y) for y in years]
         elif dategroup == 'day':
-            dategroups = [datetime.date(y, m, d)
-                          for y in years
-                          for m in months
-                          for d in xrange(1, calendar.monthrange(y, m)[1] + 1)
-                          ]
+            balance_dates = [datetime.date(y, m, d)
+                             for y in years
+                             for m in months
+                             for d in xrange(1, calendar.monthrange(y, m)[1] + 1)
+                             ]
+            dategroups = [d.isoformat() for d in balance_dates]
         else:
             # dategroup == 'month' or other
-            dategroups = [datetime.date(y, m, calendar.monthrange(y, m)[1])
+            balance_dates = [datetime.date(y, m, calendar.monthrange(y, m)[1])
+                             for y in years for m in months]
+            dategroups = ['%04d-%02d' % (y, m)
                           for y in years for m in months]
 
         # Calculate the balance of each requested account for each
         if accounts:
             whereclause.append("ARRAY[account] <@ %(accounts)s")
             args['accounts'] = accounts
-        for dg in dategroups:
-
+        for bd, dg in zip(balance_dates, dategroups):
             # Grab ledger from transactions and obligations together
-            wc = whereclause + ["postdate <= %(dategroup)s"]
-            args['dategroup'] = dg
+            wc = whereclause + ["postdate <= %(balance_date)s"]
+            args['balance_date'] = bd
             sql = ("SELECT account, SUM(amount) AS balance "
                    "FROM fullledger WHERE " +  " AND ".join(wc) +
                    " GROUP BY 1;")
             for row in db.execute(sql, args).fetchall():
-                balances.setdefault(row.account, {})[dg.isoformat()] = row.balance
+                balances.setdefault(row.account, {})[dg] = row.balance
 
         b['data'] = balances
 

flowrate/accounts.html

 
 //                                   OTHER                                   //
 
+balances = [];
+
 google.load('visualization', '1.0', {'packages':['corechart']});
 
 function drawChart() {
         var j = JSON.parse(h.responseText);
         if (keys(j.data).length == 0) return;
 
-        // Create the data table.
-        var data = new google.visualization.DataTable();
-        data.addColumn('string', 'Account');
-
-        // Add columns from our balance data
         for (var a in j.data) {
-            data.addColumn(
-                'number', a + ': ' + (accounts[a] ? accounts[a].name : ''));
+            var dgdata = j.data[a];
+            var acctid = parseInt(a);
+            for (var i = 0; i < balances.length; i++) {
+                if (balances[i][0].indexOf(acctid) != -1) {
+                    var dgsum = balances[i][1];
+                    for (var dg in dgdata) {
+                        dgsum[dg] = (dgsum[dg] || 0) + dgdata[dg];
+                    }
+                }
+            }
         }
 
-        // Calculate the number of rows and their horizontal axis values
-        var dgs = [];
-        for (var a in j.data) {
-            accountbucket = j.data[a];
-            for (var dg in accountbucket) {
-                dgs.push(dg);
+        var bc = drawAccountChart('Balances', balances, $('chart_div'));
+        var table = bc[0];
+        var chart = bc[1];
+
+        // Show transactions matching a clicked point or column (account set)
+        function handle_select() {
+            var selection = chart.getSelection();
+            var seldg = null;
+            var selaccts = null;
+            for (var i = 0; i < selection.length; i++) {
+                // We are assuming one selection for now
+                var item = selection[i];
+                if (item.row != null && item.column != null) {
+                    seldg = table.getValue(item.row, 0);
+                    selaccts = balances[item.column - 1][0];
+                } else if (item.row != null) {
+                    seldg = table.getValue(item.row, 0);
+                } else if (item.column != null) {
+                    selaccts = balances[item.column - 1][0];
+                }
             }
-            break;
-        }
-        dgs.sort();
 
-        // Add rows from our balance data
-        for (var i = 0; i < dgs.length; i++) {
-            var row = [dgs[i]];
-            for (var a in j.data) {
-                row.push(j.data[a][dgs[i]]);
+            var qs = [];
+            if (selaccts != null) qs.push("accounts=" + selaccts.join(","));
+            if (seldg != null) {
+                qs.push("years=" + seldg.substring(0, 4));
+                if (seldg.length >= 7) {
+                    qs.push("months=" + parseInt(seldg.substring(5, 7)));
+                }
+                if (seldg.length >= 10) {
+                    qs.push("days=" + parseInt(seldg.substring(8, 10)));
+                }
             }
-            data.addRow(row);
+            window.open("/transactions/manager?" + qs.join("&"));
         }
 
-        // Set chart options
-        var width = 1200;
-        if (dgs.length <= 16) { width = dgs.length * 75; }
-        if (dgs.length < 4) { width = 400; }
-        var options = {'title': 'Balances',
-                       'width': width, 'height': 400,
-                       'vAxis': {'minValue': 0}};
+        google.visualization.events.addListener(chart, 'select', handle_select);
 
-        // Instantiate and draw our chart, passing in some options.
-        var chart = new google.visualization.LineChart($('chart_div'));
-        chart.draw(data, options);
     }
     h.send();
 }
     $("flowslink").href = $("flowslink").href + "?" + qs;
 
     populate_accounts();
+
+    var req_accounts = (query['accounts'] + ',' + query['credits'] + ','
+                        + query['debits']).split(",");
+    var acctsets = account_sets(req_accounts);
+    for (var i=0; i < acctsets.length; i++) {
+        balances.push([acctsets[i], {}])
+    }
     drawChart();
 
     edit_account(null);

flowrate/common.js

+//                                 UTILITIES                                 //
+
+function $(name) { return document.getElementById(name) };
+
+function commafy(text) {
+    return text.toString().replace(/\d(?=(?:\d\d\d)+(?!\d))/g, "$&,");
+}
+
+function truncate(text, len) {
+    if (text.length > len) text = text.substring(0, len - 3) + '...';
+    return text;
+}
+
+function option_text(options, value) {
+    // Return .text from the option whose .value matches the given value,
+    // otherwise return the original value
+    for (var i = 0; i < options.length; i++) {
+        if (options[i].value == value) {
+            return options[i].text;
+        }
+    }
+    return value;
+}
+
+function ISODate(d) {
+    var dy = d.getFullYear()
+    // Y2K
+    if (dy < 1970) dy = dy + 100;
+    var dm = (d.getMonth() + 1).toString();
+    if (dm.length < 2) dm = "0" + dm;
+    var dd = d.getDate().toString();
+    if (dd.length < 2) dd = "0" + dd;
+    return dy + "-" + dm + "-" + dd;
+}
+
+function keys(obj) {
+   var k = [];
+   for (var key in obj){
+      k.push(key);
+   }
+   return k;
+}
+
+function segment(str, index, separator) {
+    // Return the indexed segment of the given string.
+    if (typeof(separator) == 'undefined') separator = '/';
+    var a = str.split(separator);
+    if (index < 0) index = a.length + index;
+    return a[index];
+}
+
+function get_text(elem) {
+    if (elem.innerText != undefined) {
+        // Internet Explorer
+        return elem.innerText;
+    } else {
+        // Mozilla
+        return elem.textContent;
+    }
+}
+
+function set_text(elem, newvalue) {
+    if (elem.innerText != undefined) {
+        // Internet Explorer
+        elem.innerText = newvalue;
+    } else {
+        // Mozilla
+        elem.textContent = newvalue;
+    }
+}
+
+function http(method, url, async, msg) {
+    var h;
+    
+    if (typeof(XMLHttpRequest) != "undefined") {
+        h = new XMLHttpRequest();
+    } else {
+        try { h = new ActiveXObject("Msxml2.XMLHTTP"); }
+        catch (e) {
+            try { h = new ActiveXObject("Microsoft.XMLHTTP"); }
+            catch (E) { set_status("Your browser is not supported.", 5000, "DarkRed"); }
+        }
+    }
+    
+    if (msg != undefined) var m = set_status(msg);
+    
+    h.onreadystatechange = function() {
+        if (h.readyState == 4) {
+            if (msg != undefined) remove_status(m);
+            try {
+                var status = h.status;
+            } catch(e) {
+                var status = "NO HTTP RESPONSE";
+            }
+            if (status in h) {
+                // Use a custom handler (defined on the XMLHttpRequest object
+                //  itself by the caller of this function).
+                h[status](h)
+            } else {
+                // Use a default handler.
+                if (status >= 500) {
+                    var v = h.status.toString() + ' ' + h.statusText;
+                    var ct = h.getResponseHeader("Content-Type");
+                    var is_html = (ct && ct.indexOf("html") != -1);
+                    set_fatal("Failure (" + v + "): " + (msg ? msg : url),
+                              h.responseText, is_html);
+                } else {
+                    var v = h.status.toString() + ' ' + h.statusText;
+                    set_status("Failure (" + v + "): " + (msg ? msg : url),
+                               5000, "DarkRed");
+                }
+            }
+        }
+    }
+    
+    h.open(method, url, async);
+    return h
+}
+
+// ----------------------------- DOMAIN HELPERS ----------------------------- //
+
+function account_sets(req_accounts) {
+    // Return a list of lists of account ids from the given comma-separated
+    // string of account ranges. "1,3-5,1+5" returns [[1], [3, 4, 5], [1, 5]]
+    var acctsets = [];
+    for (var i=0; i < req_accounts.length; i++) {
+        // Strip out any chars besides 0-9, plus and dash
+        var linespec = req_accounts[i].replace(/[^0-9+-]/g, '')
+        if (linespec) {
+            var acctids = [];
+            var idranges = linespec.split('+');
+            for (var j=0; j < idranges.length; j++) {
+                var idrange = idranges[j].split("-", 2);
+                if (idrange.length == 2) {
+                    // "lo-hi" = a range of ids from lo to hi, inclusive
+                    var lo = parseInt(idrange[0]);
+                    var hi = parseInt(idrange[1]);
+                    if (isNaN(lo) || isNaN(hi) || lo >= hi) continue;
+                    for (var k=lo; k <= hi; k++) {
+                        if (acctids.indexOf(k) == -1) acctids.push(k);
+                    }
+                } else {
+                    // single id
+                    var k = parseInt(idrange[0]);
+                    if (isNaN(k)) continue;
+                    if (acctids.indexOf(k) == -1) acctids.push(k);
+                }
+            }
+            acctsets.push(acctids);
+        }
+    }
+    return acctsets;
+}
+
+function isSubAccount(child, parent) {
+    for (var i = 3; i >= 0; i--) {
+        var scale = Math.pow(10, i);
+        if ((parent % scale == 0) && (parent <= child) && (child < parent + scale)) {
+            return true;
+        }
+    }
+    return false;
+}
+
+
+//                                   STATUS                                   //
+
+function set_fatal(title, msg, is_html) {
+    set_text($("fatal_error_title"), title);
+    if (is_html) {
+        $("fatal_error_msg").innerHTML = msg;
+    } else {
+        set_text($("fatal_error_msg"), msg);
+    }
+    $("fatal_error").style.zIndex = 100;
+    $("fatal_error").style.visibility = 'visible';
+}
+
+function remove_fatal() {
+    set_text($("fatal_error_title"), "");
+    $("fatal_error_msg").innerHTML = "";
+    $("fatal_error").style.visibility = 'hidden';
+    $("fatal_error").style.zIndex = -1;
+}
+
+function set_status(msg, decay, color) {
+    var log = $("status");
+    var m = document.createElement("p");
+    if (color != undefined) m.style.backgroundColor = color;
+    set_text(m, msg);
+    log.appendChild(m);
+    if (!(decay != undefined)) decay = 5000;
+    setTimeout(function () { remove_status(m); }, decay);
+    return m;
+}
+
+function remove_status(msgelem) {
+    if (msgelem.parentNode) msgelem.parentNode.removeChild(msgelem);
+}
+
+//                                   CHART                                   //
+
+google.load('visualization', '1.0', {'packages':['corechart']});
+
+function drawAccountChart(title, data, element) {
+    // Draw a Google Chart onto the $chart_div element.
+    // The 'data' argument MUST be an array of (account set, dategroup) pairs.
+    // Each account set must an array of account id's (integers), which will
+    // be used as columns in the DataTable, which then are drawn as individual
+    // lines/bars in the resulting chart.
+    // Each dategroup is an object with string keys that describe a date group,
+    // (such as '2012-01' for the month of January, 2012), which will be used
+    // as rows in the DataTable, which then are drawn as individual points
+    // on the horizontal axis.
+
+    // Create the data table.
+    var table = new google.visualization.DataTable();
+    table.addColumn('string', 'Account');
+
+    // Add columns from our data
+    var series = {};
+    var dategroups = [];
+    for (var i = 0; i < data.length; i++) {
+        var col = data[i];
+        var accountset = col[0]
+        if (accountset.length > 1) {
+            // Draw a line, label it with ids only
+            table.addColumn('number', accountset.join(" + "));
+            series[i] = {"type": "line", "curveType": "function"};
+        } else {
+            // Draw bars, label it with id and name
+            var a = accountset[0];
+            var name = (accounts[a] ? accounts[a].name : '');
+            table.addColumn('number', a + ': ' + name);
+        }
+
+        // Grab the row keys and give them a stable sort order
+        if (i == 0) {
+            for (var dg in col[1]) {
+                dategroups.push(dg);
+            }
+            dategroups.sort();
+        }
+    }
+
+    // Add rows from our data
+    for (var i = 0; i < dategroups.length; i++) {
+        var dg = dategroups[i];
+        var row = [dg];
+        for (var a = 0; a < data.length; a++) {
+            row.push(data[a][1][dg] || 0);
+        }
+        table.addRow(row);
+    }
+
+    // For some reason, we have to do this after we add all the rows
+    var formatter = new google.visualization.NumberFormat(
+        {prefix: '$', negativeColor: 'red', negativeParens: true});
+    for (var i = 0; i < data.length; i++) {
+        formatter.format(table, i + 1);
+    }
+
+    var width = dategroups.length * 75;
+    if (width < 600) { width = 600; }
+    if (width > 1200) { width = 1200; }
+    var options = {'title': title,
+                   'width': width, 'height': 400,
+                   'vAxis': {'minValue': 0},
+                   'seriesType': "bars",
+                   'series': series
+                   };
+
+    // Instantiate and draw our chart, passing in some options.
+    var chart = new google.visualization.ComboChart(element);
+    chart.draw(table, options);
+
+    return [table, chart];
+}
+

flowrate/transactions.html

     var c_acct = accounts[ca];
     var da = parseInt(segment(tx.debit, -1));
     var d_acct = accounts[da];
-    for (var a=0; a < activity.length; a++) {
+    for (var a = 0; a < activity.length; a++) {
         if (activity[a][0].indexOf(ca) != -1) {
             if (c_acct.type == 'asset' || c_acct.type == 'expense') {
                 activity[a][1][dg] -= tx.amount;
 //                                   OTHER                                   //
 
 
-google.load('visualization', '1.0', {'packages':['corechart']});
-
 function drawChart() {
     if (activity.length == 0) return;
 
-    // Create the data table.
-    var data = new google.visualization.DataTable();
-    data.addColumn('string', 'Account');
-
-    // Add columns from our balance data
-    var series = {};
-    for (var i=0; i < activity.length; i++) {
-        var col = activity[i];
-        if (col[0].length > 1) {
-            // Draw a line, label it with ids only
-            data.addColumn('number', col[0].join(" + "));
-            series[i] = {"type": "line", "curveType": "function"};
-        } else {
-            // Draw bars, label it with id and name
-            var a = col[0][0];
-            var name = (accounts[a] ? accounts[a].name : '');
-            data.addColumn('number', a + ': ' + name);
-        }
-    }
-
-    activity_dategroups.sort();
-
-    // Add rows from our balance data
-    for (var i = 0; i < activity_dategroups.length; i++) {
-        var dg = activity_dategroups[i];
-        var row = [dg];
-        for (var a=0; a < activity.length; a++) {
-            var col = activity[a];
-            var v = col[1][dg] || 0;
-            row.push(v);
-        }
-        data.addRow(row);
-    }
-
-    // For some reason, we have to do this after we collect the data
-    var formatter = new google.visualization.NumberFormat(
-        {prefix: '$', negativeColor: 'red', negativeParens: true});
-    for (var i=0; i < activity.length; i++) {
-        formatter.format(data, i + 1);
-    }
-
-    var width = activity_dategroups.length * 75;;
-    if (width < 600) { width = 600; }
-    if (width > 1200) { width = 1200; }
-    var options = {'title': 'Activity',
-                   'width': width, 'height': 400,
-                   'vAxis': {'minValue': 0},
-                   'seriesType': "bars",
-                   'series': series
-                   };
-
-    // Instantiate and draw our chart, passing in some options.
-    var chart = new google.visualization.ComboChart($('chart_div'));
-    chart.draw(data, options);
+    var ac = drawAccountChart('Activity', activity, $('chart_div'));
+    var table = ac[0];
+    var chart = ac[1];
 
     // Highlight rows matching a clicked point or column (account set)
     function handle_select() {
             // We are assuming one selection for now
             var item = selection[i];
             if (item.row != null && item.column != null) {
-                seldg = activity_dategroups[item.row];
+                seldg = table.getValue(item.row, 0);
                 selaccts = activity[item.column - 1][0];
             } else if (item.row != null) {
-                seldg = activity_dategroups[item.row];
+                seldg = table.getValue(item.row, 0);
             } else if (item.column != null) {
                 selaccts = activity[item.column - 1][0];
             }
 
     var req_accounts = (query['accounts'] + ',' + query['credits'] + ','
                         + query['debits']).split(",");
-    acctsets = account_sets(req_accounts);
+    var acctsets = account_sets(req_accounts);
     for (var i=0; i < acctsets.length; i++) {
         activity.push([acctsets[i], {}])
     }