-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmint_to_sankey.js
169 lines (153 loc) · 6.64 KB
/
mint_to_sankey.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
function prepData(dat, incomeParent) {
/*
* Prepare data for use with Sankey plot.
*
* @param dat {obj} : input data of the form
* @param incomeParent {str} : name of parent group for all income, this
* node is used to join credit transactions to debit transactions. If
* not specified, all transactions are considered debit.
*
* @return {obj} : data for Sankey chart in the form
* {"nodes":
* [
* {"name": "Utilities", "Transaction Type": "debit"},
* {"name": "Rent", "Transaction Type": "debit"},
* {"name": "Bills & Utilities", "Transaction Type": "debit"}
* ],
* "links":
* [
* {"source": "Bills & Utilities", "target": "Rent", "value":932.3},
* {"source": "Bill & Utilities", "target": "Utilities", "value":74.00}
* ]
* }
*/
var nodes = [], links = [];
var debitSum = {};
// Input data must have, at a minimum, the following object properties:
// - `Amount` : the transaction amount
// - `Transaction Type` : either 'debit' or 'credit'
// - `Category` : transaction category (e.g. 'Utilities')
// Any other properties will simply be passed onto the node in the chart.
//
// Groups defines how transaction categories are grouped together.
//
// For example the group:
// `'Travel': ['Rental Car & Taxi','Hotel','Air Travel']`
// will place all transactions with the Category 'Rental Car & Tax',
// 'Hotel' or 'Air Travel' into the parent group 'Travel'. Furthermore,
// any transactions with the Category 'Travel' will also be placed into
// the 'Travel' parent group since Mint won't always properly categorize
// the transaction into one of the sub-groups.
// Any transactions with categories not listed in any of the groups will
// simply be ignored.
var groups = {
'Auto & Transport': ['Gas & Fuel','Parking','Service & Parts','Public Transportation','Auto Insurance','Auto Payment'],
'Bills & Utilities': ['Internet','Utilities','Mobile Phone','Rent','Home Phone'],
'Business Services': ['Shipping','Web hosting'],
'Education': ['Books & Supplies'],
'Entertainment': ['Movies & DVDs','Music','Newspapers & Magazines','Amusement','Entertainment'],
'Fees & Charges': ['Finance Charge','Service Fee','ATM Fee'],
'Financial': ['Investment','529','Cryptocurrency','Financial Advisor','Life Insurance'],
'Food & Dining': ['Groceries','Coffee Shops','Fast Food','Restaurants','Alcohol & Bars'],
'Gifts & Donations': ['Gift','Charity'],
'Health & Fitness': ['Gym','Pharmacy','Doctor','Sports','Eyecare'],
'Home': ['Furnishings','Home Improvement','Lawn & Garden','Home Supplies','Home Services'],
'Misc Expenses': ['Cash Withdrawel'],
'Personal Care': ['Hair'],
'Pets': ['Pet Food & Supplies'],
'Shopping': ['Hobbies','Electronics & Software','Clothing','Sporting Goods'],
'Taxes': ['Property Tax','Federal Tax','State Tax'],
'Travel': ['Rental Car & Taxi','Hotel','Air Travel'],
'Uncategorized': ['Cash & ATM','Check']
}
if (typeof(incomeParent) !== 'undefined') {
groups[incomeParent] = ['Interest Income', 'Freelance', 'Reimbursement', 'Paycheck']
}
for (var group in groups) {
// filter down to data of interest
keep_category = groups[group]
keep_category.push(group);
var filtered = dat.filter(function(d, i) {
if (group == 'Financial' && d.Category == '529') {
}
return keep_category.indexOf(d.Category) !== -1;
})
// if we've filtered everything out
// because these categories don't exist
if (filtered.length == 0) break
// naively take the first transaction to set
// the transaction type
var type = filtered[0]['Transaction Type']
// group income on Category and sum all transaction amounts
var nested = d3.nest()
.key(function (d) { return d.Category; })
.rollup(function(group) {
return {
'sum': d3.sum(group, function(d) { return d.Amount}),
'meta': group[0]
}
})
.entries(filtered)
// sum value for parent debit group
// in order to make the link between
// debit and credit
if (type == 'debit') {
debitSum[group] = d3.sum(nested, function(d) { return d.values.sum })
}
// generate nodes
// some transactions may be categorized by the parent category
// (e.g. Bill & Utilities), therefore we append a '_' to the
// parent category to make it distinct, this acts as the source
// for a transaction
nodes.push({
'name': '_'+group,
'Transaction Type': type
}) // manually at parent node
nested.forEach(function (d) {
var node = {'name': d.key}
for (var i in d.values.meta) {
if (i !== 'Amount') node[i] = d.values.meta[i];
}
nodes.push(node);
})
// generate links
nested.forEach(function (d) {
links.push({
'target': type == 'debit' ? d.key : '_'+group,
'source': type == 'credit' ? d.key : '_'+group,
'value': d.values.sum
});
})
}
var all_nodes = [];
links.forEach(function(d) {
if (all_nodes.indexOf(d.source) == -1) all_nodes.push(d.source);
if (all_nodes.indexOf(d.target) == -1) all_nodes.push(d.target);
})
// remove any nodes without links
nodes = nodes.filter(function(node) {
return all_nodes.indexOf(node.name) != -1;
})
// add links between credit and debit if incomeParent specified
// otherwise assume all transaction are debit and manually
// generate an 'All debit' parent node.
if (typeof(incomeParent) !== 'undefined') {
for (var group in debitSum) {
links.push({
'source': '_'+incomeParent,
'target': '_'+group,
'value': debitSum[group]
})
}
} else {
for (var group in debitSum) {
links.push({
'source': 'All debit',
'target': '_'+group,
'value': debitSum[group]
})
}
nodes.push({'name':'All debit'})
}
return {'nodes':nodes, 'links':links}
}