Commits

Robert Brewer committed f7512f6

New budget graph on flows.html. Made obligations.remaining static. Refactored flows.Flow a bit.

  • Participants
  • Parent commits 3e7b069

Comments (0)

Files changed (7)

flowrate/__init__.py

             (vals['amount'], credit, debit,
              vals['postdate'], vals['description'], req.txid)).fetchone()
 
+        flows.unfulfill(txrow.id)
         flows.fulfill(txrow)
 
         cherrypy.response.status = 204
     def DELETE(self):
         """Destroy the definition of this transaction."""
         req = cherrypy.serving.request
-        db.execute("DELETE FROM fulfillments WHERE transactionid = %s; "
-                   "DELETE FROM transactions WHERE id = %s;",
-                   (req.txid, req.txid))
+        flows.unfulfill(req.txid)
+        db.execute("DELETE FROM transactions WHERE id = %s;", (req.txid,))
         cherrypy.response.status = 204
 
 
         return page % {'body': body}
 
 
+class Budgets(object):
+
+    exposed = True
+    description = "A table of account budgets."
+
+    @cherrypy.tools.json_out()
+    def GET(self, accounts='', years='', months='', days='', dategroup='month',
+            **kwargs):
+        """Return a table of budgets."""
+        b = {
+            "self": cherrypy.url(qs=cherrypy.request.query_string),
+            "description": self.description % vars(),
+            "views": {"filtered": "{?accounts+,years+,months+,days+.dategroup}"},
+            }
+
+        accounts = toInts(accounts)
+        years = toInts(years)
+        months = toInts(months)
+        days = toInts(days)
+
+        if not (accounts or years or months or days):
+            # No filters. Return nothing rather than everything.
+            return b
+
+        # Determine the full range of buckets; even if our ledger has no
+        # transactions within a given bucket, we still want to output it.
+        if not years:
+            years = [row.year for row in db.execute(
+                "SELECT DISTINCT EXTRACT(year FROM postdate) AS year "
+                "FROM transactions;").fetchall()]
+            if not years:
+                years = [datetime.date.today().year]
+        if not months:
+            months = range(1, 13)
+
+        if dategroup == 'year':
+            budget_dates = [datetime.date(y, 12, 31) for y in years]
+            dategroups = [str(y) for y in years]
+            pgdatefmt = 'YYYY'
+        elif dategroup == 'day':
+            budget_dates = []
+            for y in years:
+                for m in months:
+                    for d in days or xrange(1, calendar.monthrange(y, m)[1] + 1):
+                        budget_dates.append(datetime.date(y, m, d))
+            dategroups = [d.isoformat() for d in budget_dates]
+            pgdatefmt = 'YYYY-MM-DD'
+        else:
+            # dategroup == 'month' or other
+            budget_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]
+            pgdatefmt = 'YYYY-MM'
+
+        # Calculate the budget of each requested account for each
+        # dategroup in the output.
+        budgets = dict((a, dict((dg, [0, 0]) for dg in dategroups))
+                       for a in accounts)
+        whereclause, args = [], {}
+        if accounts:
+            whereclause.append("ARRAY[account] <@ %(accounts)s")
+            args['accounts'] = accounts
+        for bd, dg in zip(budget_dates, dategroups):
+            wc = whereclause + [
+                "to_char(postdate, %(df)s) = %(dg)s"]
+            args['df'] = pgdatefmt
+            args['dg'] = dg
+            # TODO: this isn't quite right; we should show amount over budget,
+            # but obligations.remaining is currently not allowed to go under 0
+            sql = ("SELECT account, SUM(total) AS budget, "
+                   "SUM(total - amount) AS fulfilled "
+                   "FROM flowledger WHERE " +  " AND ".join(wc) +
+                   " GROUP BY 1 ORDER BY 1;")
+            print sql
+            for row in db.execute(sql, args).fetchall():
+                budgets[row.account][dg] = [row.budget, row.fulfilled]
+
+        b['data'] = budgets
+
+        return b
+
+
 class Flow(object):
     
     exposed = True
         """Return the flow."""
         req = cherrypy.serving.request
 
-        flow = flows.Flow()
-        flow.find(req.flowid)
+        flow = flows.Flow(req.flowid)
+        flow.find()
         if flow.row is None:
             raise cherrypy.NotFound()
 
         """Update the given flow entity."""
         req = cherrypy.serving.request
 
-        flow = flows.Flow()
-        flow.find(req.flowid)
+        flow = flows.Flow(req.flowid)
+        flow.find()
         if flow.row is None:
             raise cherrypy.NotFound()
 
             "WHERE id = %s RETURNING *;",
             (vals['amount'], credit, debit, vals['start'], vals['end'],
             vals['period'], vals['unit'], vals['days'], vals['description'],
-            req.flowid)).fetchone()
+            flow.id)).fetchone()
 
         flow.clear_obligations()
         flow.obligate()
         """Destroy the definition of this flow."""
         req = cherrypy.serving.request
 
-        # Remove all existing obligations
-        # (and their fulfillments) for this flow
-        db.execute(
-            "DELETE FROM fulfillments "
-            "WHERE obligationid"
-            " IN (SELECT id FROM obligations WHERE flowid = %s); "
-
-            "DELETE FROM obligations WHERE flowid = %s;",
-            (req.flowid, req.flowid))
-
-        db.execute("DELETE FROM flows WHERE id = %s;", (req.flowid,))
+        f = flows.Flow(req.flowid)
+        f.clear_obligations()
+        f.delete()
         cherrypy.response.status = 204
 
 flowsmanager = cherrypy.tools.staticfile.handler(
     
         # Accept the new flow definition.
         vals = req.json["body"]
-        flow = flows.Flow()
-        flow.row = db.execute("INSERT INTO flows"
-                   " (amount, credit_account, debit_account, "
-                   "range_start, range_end, period, unit, days, description) "
-                   "VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING *",
-                   (vals["amount"],
-                    popint(vals["credit"]), popint(vals["debit"]),
-                    vals["start"], vals["end"],
-                    vals["period"], vals['unit'], vals["days"],
-                    vals["description"],
-                    )).fetchone()
-
+        vals["credit"] = popint(vals["credit"])
+        vals["debit"] = popint(vals["debit"])
+        flow = flows.Flow.insert(**vals)
         flow.obligate()
 
         cherrypy.response.status = 201
 
     accounts = Accounts()
     balances = Balances()
+    budgets = Budgets()
     flows = Flows()
     transactions = Transactions()
     json2_js = json2_js

flowrate/flowrate.sql

     credit_account integer NOT NULL,
     debit_account integer NOT NULL,
     description text,
-    amount numeric(10, 2) NOT NULL
+    amount numeric(10, 2) NOT NULL,
+    remaining numeric(10, 2) NOT NULL
 );
 
 
 -- View: flowledger
 
 CREATE OR REPLACE VIEW flowledger AS
-    WITH o AS (
-        SELECT o1.*,
-        amount - (SELECT COALESCE(SUM(amount), 0) FROM fulfillments f
-         WHERE f.obligationid = o1.id) AS remaining
-        FROM obligations o1
-        WHERE o1.postdate >= (CURRENT_DATE - '1 month'::interval)
-        )
     SELECT postdate,
            credit_account AS account,
            (CASE WHEN a1.type IN ('asset', 'expense')
+                 THEN 0 - amount ELSE amount
+            END) AS total,
+           (CASE WHEN a1.type IN ('asset', 'expense')
                  THEN 0 - remaining ELSE remaining
             END) AS amount
-    FROM o LEFT JOIN accounts a1 ON o.credit_account = a1.id
-    WHERE remaining > 0
+    FROM obligations LEFT JOIN accounts a1 ON obligations.credit_account = a1.id
     UNION ALL
     SELECT postdate,
            debit_account AS account,
            (CASE WHEN a2.type IN ('asset', 'expense')
+                 THEN amount ELSE 0 - amount
+            END) AS total,
+           (CASE WHEN a2.type IN ('asset', 'expense')
                  THEN remaining ELSE 0 - remaining
             END) AS amount
-    FROM o LEFT JOIN accounts a2 ON o.debit_account = a2.id
-    WHERE remaining > 0
+    FROM obligations LEFT JOIN accounts a2 ON obligations.debit_account = a2.id
     ORDER BY 1, 2;
 
 ALTER TABLE flowledger OWNER TO postgres;
 
 CREATE OR REPLACE VIEW fullledger AS
     SELECT postdate, account, amount FROM ledger
+
     UNION ALL
+
     SELECT postdate, account, amount FROM flowledger
+    -- Since this table is used for balances, don't include
+    -- flow activity in the past (we include last month because
+    -- we assume some transactions haven't been posted yet).
+    WHERE flowledger.postdate >= (CURRENT_DATE - '1 month'::interval)
+    AND flowledger.amount > 0
+
     ORDER BY 1, 2;
 
 ALTER TABLE fullledger OWNER TO postgres;
 
 
-
 --
 -- Name: public; Type: ACL; Schema: -; Owner: postgres
 --

flowrate/flows.py

 
 class Flow(object):
 
-    def __init__(self):
+    def __init__(self, id):
+        self.id = id
         self.row = None
 
-    def find(self, flowid):
+    def find(self):
         """Set self.row to a DB row matching the given ID, or None."""
         self.row = flowrate.db.execute(
-            "SELECT * FROM flows WHERE id = %s;", (flowid,)).fetchone()
+            "SELECT * FROM flows WHERE id = %s;", (self.id,)).fetchone()
+
+    @classmethod
+    def insert(cls, **vals):
+        flow = cls(None)
+        flow.row = flowrate.db.execute("INSERT INTO flows"
+                   " (amount, credit_account, debit_account, "
+                   "range_start, range_end, period, unit, days, description) "
+                   "VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING *",
+                   (vals["amount"],
+                    vals["credit"], vals["debit"],
+                    vals["start"], vals["end"],
+                    vals["period"], vals['unit'], vals["days"],
+                    vals["description"],
+                    )).fetchone()
+        flow.id = flow.row.id
+        return flow
+
+    def delete(self):
+        db.execute("DELETE FROM flows WHERE id = %s;", (self.id,))
 
     def __getattr__(self, key):
         return getattr(self.row, key)
 
     def obligations(self):
         """Yield obligation rows for the given flow."""
-        # Add dummy transactions for this flow based on its unit
         if self.unit == 'months':
             unit, day = month.fromdate, lambda d: d.day
         elif self.unit == 'weeks':
             row = flowrate.db.execute(
                 "INSERT INTO obligations "
                 "(flowid, postdate, credit_account, debit_account,"
-                " description, amount, dategroupformat) "
-                "VALUES (%s, %s, %s, %s, %s, %s, %s) RETURNING id;",
+                " description, amount, remaining, dategroupformat) "
+                "VALUES (%s, %s, %s, %s, %s, %s, 0.0, %s) RETURNING id;",
                 (self.id, ob['postdate'], ob['credit'], ob['debit'],
                  ob['description'], ob['amount'], dategroupformat)).fetchone()
             obs[row.id] = ob
                         "VALUES (%s, %s, %s);", (tx.id, obid, f_amt))
                     obrem -= f_amt
                     if obrem <= 0:
+                        # Don't allow obligations.remaining to be < 0, below
+                        obrem = 0
                         break
 
+            flowrate.db.execute(
+                "UPDATE obligations SET remaining = %s "
+                "WHERE id = %s;", (obrem, obid))
+
     def clear_obligations(self):
         """Remove all existing obligations (and their fulfillments) for self."""
         # TODO: This isn't quite right; it only deletes existing fulfillments
     than for income. They are then ordered by date, ascending, within each
     debit account.
     """
-    flowrate.db.execute(
-        "DELETE FROM fulfillments WHERE transactionid = %s", (txrow.id,))
-
     if txrow.amount <= 0:
         return
 
-    obs = [(row.id, row.amount, row.fulfilled)
+    obs = [(row.id, row.amount, row.remaining)
            for row in flowrate.db.execute(
-                "SELECT o.*, "
-                "(SELECT COALESCE(SUM(f.amount), 0) FROM fulfillments f"
-                " WHERE f.obligationid = o.id) AS fulfilled "
-                "FROM obligations o "
-                "WHERE isSubAccount(%s, o.credit_account) "
-                "AND isSubAccount(%s, o.debit_account) "
-                "AND (to_char(%s, o.dategroupformat) = "
-                     "to_char(o.postdate, o.dategroupformat)) "
-                "ORDER BY o.debit_account DESC, o.postdate ASC",
+                "SELECT * FROM obligations "
+                "WHERE isSubAccount(%s, credit_account) "
+                "AND isSubAccount(%s, debit_account) "
+                "AND (to_char(%s, dategroupformat) = "
+                     "to_char(postdate, dategroupformat)) "
+                "ORDER BY debit_account DESC, postdate ASC",
                 (txrow.credit_account, txrow.debit_account,
                  txrow.postdate)).fetchall()]
 
     txrem = txrow.amount
-    for obid, obamount, obfulfilled in obs:
-        obrem = obamount - obfulfilled
+    for obid, obamount, obrem in obs:
         f_amt = min(obrem, txrem)
         if f_amt > 0:
             flowrate.db.execute(
                 "INSERT INTO fulfillments "
                 "(transactionid, obligationid, amount) VALUES (%s, %s, %s);",
                 (txrow.id, obid, f_amt))
+            flowrate.db.execute(
+                "UPDATE obligations SET remaining = remaining - %s "
+                "WHERE id = %s;", (f_amt, obid))
             txrem -= f_amt
             if txrem <= 0:
                 break
 
+def unfulfill(txid):
+    """Delete any fulfillments for the given transaction. Update obligations."""
+    obids = [row.obligationid for row in flowrate.db.execute(
+        "DELETE FROM fulfillments "
+        "WHERE transactionid = %s RETURNING obligationid;",
+        (txid,)).fetchall()]
+    for obid in obids:
+        flowrate.db.execute(
+            "UPDATE obligations o SET o.remaining = o.amount - "
+            "(SELECT COALESCE(SUM(f.amount), 0) FROM fulfillments f"
+            " WHERE f.obligationid = o.id) "
+            "WHERE o.id = %s;", (obid,))
+
 
 def transactions(accounts=None, credits=None, debits=None,
                  years=None, months=None, days=None,
     """Yield flow transactions matching the given criteria (by postdate desc)."""
     whereclause, args = [], {}
     if accounts:
-        whereclause.append("(ARRAY[o.credit_account] <@ %(accounts)s"
-                           " OR ARRAY[o.debit_account] <@ %(accounts)s)")
+        whereclause.append("(ARRAY[credit_account] <@ %(accounts)s"
+                           " OR ARRAY[debit_account] <@ %(accounts)s)")
         all_accts = [row.id for row in flowrate.db.execute(
                      "SELECT id FROM accounts;").fetchall()]
         args['accounts'] = [a for a in all_accts if isSubAccount(a, accounts)]
     if credits:
-        whereclause.append("ARRAY[o.credit_account] <@ %(credits)s")
+        whereclause.append("ARRAY[credit_account] <@ %(credits)s")
         args['credits'] = credits
     if debits:
-        whereclause.append("ARRAY[o.debit_account] <@ %(debits)s")
+        whereclause.append("ARRAY[debit_account] <@ %(debits)s")
         args['debits'] = debits
     if description:
-        whereclause.append("o.description ILIKE %(desc)s")
+        whereclause.append("description ILIKE %(desc)s")
         args['desc'] = '%' + description + '%';
     if years:
-        whereclause.append("ARRAY[EXTRACT(year FROM o.postdate)::integer] <@ %(years)s")
+        whereclause.append("ARRAY[EXTRACT(year FROM postdate)::integer] <@ %(years)s")
         args['years'] = years
     if months:
-        whereclause.append("ARRAY[EXTRACT(month FROM o.postdate)::integer] <@ %(months)s")
+        whereclause.append("ARRAY[EXTRACT(month FROM postdate)::integer] <@ %(months)s")
         args['months'] = months
     if days:
-        whereclause.append("ARRAY[EXTRACT(day FROM o.postdate)::integer] <@ %(days)s")
+        whereclause.append("ARRAY[EXTRACT(day FROM postdate)::integer] <@ %(days)s")
         args['days'] = days
 
     if not whereclause:
         return
 
     for ob in flowrate.db.execute(
-        "SELECT o.*, "
-        "(SELECT COALESCE(SUM(f.amount), 0) FROM fulfillments f"
-        " WHERE f.obligationid = o.id) AS fulfilled "
-        "FROM obligations o "
+        "SELECT * FROM obligations "
         "WHERE " + " AND ".join(whereclause) +
         # Don't let old unfulfilled obligations screw up balances
-        " AND o.postdate >= (CURRENT_DATE - '1 month'::interval) "
-        "ORDER BY o.postdate DESC;", args).fetchall(
+        " AND postdate >= (CURRENT_DATE - '1 month'::interval)"
+        " AND remaining > 0 "
+        "ORDER BY postdate DESC;", args).fetchall(
         ):
-        amount = ob.amount - ob.fulfilled
-        if amount > 0:
-            yield {
-                'postdate': ob.postdate,
-                'credit': ob.credit_account,
-                'debit': ob.debit_account,
-                'description': ob.description,
-                'amount': amount,
-                }
+        yield {
+            'postdate': ob.postdate,
+            'credit': ob.credit_account,
+            'debit': ob.debit_account,
+            'description': ob.description,
+            'amount': ob.remaining,
+            }
 

flowrate/testing/test.cfg

     "host": "localhost",
     "superuser": "postgres",
     "template": "template1",
-    "database": "flowrate",
+    "database": "flowrate_test",
     "user": "postgres",
     "password": "",
     "port": 5434
     },
  "host": "localhost",
- "port": 8090
+ "port": 8091
 }
 

flowrate/ui/accounts.html

 
 balances = [];
 
-google.load('visualization', '1.0', {'packages':['corechart']});
-
 function drawChart() {
     // Get the balances data
     var h = http("GET", "/balances" + document.location.search, true,

flowrate/ui/common.js

 
 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.
+    // 
+    // The 'data' argument MUST be an array of
+    // (account set, dataset[, titlesuffix]) tuples.
+    // 
     // 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.
+    // 
+    // Each dataset is an object. Each key MUST be a string that describes a
+    // date group, such as '2012-01' for the month of January, 2012. These will
+    // be used to define rows in the DataTable and drawn as individual points
+    // on the horizontal axis. Each dataset value is then drawn as a coordinate
+    // in the chart.
+    // 
+    // If a titlesuffix is provided, it MUST be a string. It will be appended
+    // to each column title.
+
+    if (data.length <= 0) return;
 
     // Create the data table.
     var table = new google.visualization.DataTable();
 
     // 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]
+
+        // Form the title of the column
+        var accountset = col[0];
         if (accountset.length > 1) {
             // Draw a line, label it with ids only
-            table.addColumn('number', accountset.join(" + "));
+            var name = 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);
+            var name = a + ': ' + (accounts[a] ? accounts[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();
-        }
+        // Append the title suffix (like ' Budget'), if provided
+        if (col[2] != undefined) name = name + col[2];
+
+        // Add the column
+        table.addColumn('number', name);
     }
 
+    // Grab the row keys and give them a stable sort order
+    var dategroups = keys(data[0][1]);
+    dategroups.sort();
+
     // Add rows from our data
     for (var i = 0; i < dategroups.length; i++) {
         var dg = dategroups[i];

flowrate/ui/flows.html

 
 </style>
 
+<script type="text/javascript" src="https://www.google.com/jsapi"></script>
 <script type="text/javascript" src="/json2.js"></script>
 <script type="text/javascript" src="/common.js"></script>
 <script type="text/javascript">
 
+google.load('visualization', '1.0', {'packages':['corechart']});
+
 //                                   FLOWS                                   //
 
 accounts = {};
 
 //                                   OTHER                                   //
 
+budgets = [];
+
+function drawChart() {
+    // Get the budgets data
+    var h = http("GET", "/budgets" + document.location.search, true,
+                 "Retrieving budget data...");
+    h[200] = function(h) {
+        var j = JSON.parse(h.responseText);
+        if (keys(j.data).length == 0) return;
+
+        console.log(JSON.stringify(j.data));
+        for (var a in j.data) {
+            var dgdata = j.data[a];
+            var acctid = parseInt(a);
+            for (var i = 0; i < budgets.length; i++) {
+                if (budgets[i][0].indexOf(acctid) != -1) {
+                    var dataset = budgets[i][1];
+                    if (budgets[i][2] == ' Budget') {
+                        for (var dg in dgdata) {
+                            dataset[dg] = (dataset[dg] || 0) + dgdata[dg][0];
+                        }
+                    } else { // ' Actual'
+                        for (var dg in dgdata) {
+                            dataset[dg] = (dataset[dg] || 0) + dgdata[dg][1];
+                        }
+                    }
+                }
+            }
+        }
+
+        var bc = drawAccountChart('Budgets', budgets, $('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 = budgets[item.column - 1][0];
+                } else if (item.row != null) {
+                    seldg = table.getValue(item.row, 0);
+                } else if (item.column != null) {
+                    selaccts = budgets[item.column - 1][0];
+                }
+            }
+
+            var qs = [];
+            if (selaccts != null) qs.push("accounts=" + selaccts.join(","));
+            if (seldg == null) {
+                qs.push("years=" + query['years']);
+                qs.push("months=" + query['months']);
+                qs.push("days=" + query['days']);
+            } else {
+                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)));
+                }
+            }
+            window.open("/transactions/manager?" + qs.join("&"), "_blank");
+        }
+
+        google.visualization.events.addListener(chart, 'select', handle_select);
+
+    }
+    h.send();
+}
+
 query = {
     'accounts': '',
     'credits': '',
     $('years').value = query['years'];
     $('months').value = query['months'];
     $('days').value = query['days'];
+    $('dategroup').value = query['dategroup'];
 
     qs = 'accounts=' + encodeURIComponent(query['accounts']) +
          '&years=' + encodeURIComponent(query['years']) +
     populate_accounts();
     populate_flows();
 
+    var req_accounts = (query['accounts'] + ',' + query['credits'] + ','
+                        + query['debits']).split(",");
+    var acctsets = account_sets(req_accounts);
+    for (var i=0; i < acctsets.length; i++) {
+        budgets.push([acctsets[i], {}, ' Budget'])
+        budgets.push([acctsets[i], {}, ' Actual'])
+    }
+    drawChart();
+
     edit_flow(null);
     $('edit_start').value = ISODate(new Date());
 }
         Credits: <input type='text' id="credits" name="credits" value="" size="9" />
         Debits: <input type='text' id="debits" name="debits" value="" size="9" />
         Years: <input type='text' id="years" name="years" value="" size="9" />
-        Month: <input type='text' id="months" name="months" value="" size="6" />
+        Months: <input type='text' id="months" name="months" value="" size="6" />
         Days: <input type='text' id="days" name="days" value="" size="6" />
+        Group: <select id="dategroup" name="dategroup">
+               <option>year</option>
+               <option selected="selected">month</option>
+               <option>day</option>
+               </select>
         <input type='submit' value="GO" />
     </form></p>
 </div>
     <p id='fatal_error_msg'></p>
 </div>
 
+<div id="chart_div"></div>
+
 <table id='flows'>
 <tr id='flowedit'>
     <td><input id='edit_save' type='button' value='Save' onClick='save_flow()' /></td>