I've hit a challenge with Sankey diagrams for my personal accounting app. The issue is that the Sankey, generated using Google Charts, won't render if the cash moves in a circle, eg from Ledger A to Ledger B to C and back to A.
The script needs needs an extra step that evaluates the data and if there's a circular movement then it should break that circle by removing the link with the lowest value.
The code starts with a dictionary of dictionaries which contains the amount of cash that's moving between each possible pair of ledgers. Zeros mean there's no cash moving between the two ledgers. This dictionary of dictionaries should be evaluated and circles broken.
Below is some code based on what @JacobDavis response. It does succeed in finding circles but not always. In the example below you can see that Ledger A leads to B. But B leads to both C and D. The code only checks C and thus misses the cycle caused by D.
The code doesn't yet try to break the cycle by removing a link. Trying to get it to identify cycles first.
all_ledgers = {
"a": {"a": 0, "b": 1, "c": 0, "d": 0, "e": 0},
"b": {"a": 0, "b": 0, "c": 1, "d": 1, "e": 0},
"c": {"a": 0, "b": 0, "c": 0, "d": 0, "e": 1},
"d": {"a": 1, "b": 0, "c": 0, "d": 0, "e": 0},
"e": {"a": 0, "b": 0, "c": 0, "d": 0, "e": 0}
}
def evaluate_for_circular(dictionary_of_dictionaries):
output = [] # Will put circular ledgers into here.
def evaluate(start,target=None,cache=None):
if not target:
target=start
if not cache:
cache=[start]
# Here we are looking at a new row and will iterate through rows until we find our target (which is same as the row ledger)
print('Evaluatating. Start: '+str(start)+'. Target: '+str(target))
for ledger in dictionary_of_dictionaries[start].keys():
# We now iterate through the items in the row. We use the keys rather than values as we're looking
# for the target.
print('Dealing with ledger '+str(ledger))
print('Cache: '+str(cache))
if dictionary_of_dictionaries[start][ledger]>0 and ledger==target:
return ledger
elif dictionary_of_dictionaries[start][ledger]>0 and ledger not in cache:
cache.append(ledger)
return evaluate(ledger,target,cache)
#return evaluate(ledger,target)
return False
for dict in dictionary_of_dictionaries.keys():
print('--')
print('Starting evaluation of row '+str(dict))
if evaluate(dict):
output.append(dict)
if output:
return output
else:
return False
q = evaluate_for_circular(all_ledgers)
if q:
print("Circle found in data: "+str(q))
else:
print("No circle found in data.")