Last active
August 3, 2019 20:07
-
-
Save thomasleveil/01fb37df8deda76c4224f39741fa1c38 to your computer and use it in GitHub Desktop.
où sont passés mes revenus ?
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<html> | |
<head> | |
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script> | |
<script type="text/javascript"> | |
/////////////////////////////////////////////////////////////////////////////////////////// | |
// | |
// Scrollez vers le bas, jusqu'à la définition de la fonction "drawChart()" | |
// | |
/////////////////////////////////////////////////////////////////////////////////////////// | |
google.charts.load('current', { 'packages': ['sankey'] }); | |
google.charts.setOnLoadCallback(drawChart); | |
const bucketStore = {} | |
class Bucket { | |
constructor(name) { | |
if (name in bucketStore) { | |
throw new Error(`bucket '${name}' alerady exist`) | |
} | |
this.name = name | |
this.in = 0 | |
this.out = 0 | |
bucketStore[name] = this | |
} | |
static get(name) { | |
if (name in bucketStore) { | |
return bucketStore[name] | |
} else { | |
return new Bucket(name) | |
} | |
} | |
get html() { | |
let valeur = null | |
if (this.in == 0 || this.out == 0) { | |
valeur = Math.abs(this.in - this.out) | |
} else if (this.in == this.out) { | |
valeur = this.in | |
} | |
if (valeur != null) { | |
return ` | |
<div class="bucketInfo"> | |
<div class="bucket-header">${this.name}</div> | |
<div class="bucket-body"> | |
<b>${valeur.toFixed(2)} €</b | |
</div> | |
</div> | |
` | |
} | |
const diff = this.in - this.out | |
let diffHtml = '' | |
if (diff < 0) { | |
diffHtml = `<p class="negatif">${diff.toFixed(2)} €</p>` | |
} else if (diff > 0) { | |
diffHtml = `<p class="positif">${diff.toFixed(2)} €</p>` | |
} | |
return ` | |
<div class="bucketInfo"> | |
<div class="bucket-header">${this.name}</div> | |
<div class="bucket-body"> | |
<ul> | |
<li><b>Entrées : </b>${this.in.toFixed(2)} €</li> | |
<li><b>Sorties : </b>${this.out.toFixed(2)} €</li> | |
</ul> | |
${diffHtml} | |
</div> | |
</div> | |
` | |
} | |
input(value) { | |
this.in += value | |
} | |
output(value) { | |
this.out += value | |
} | |
} | |
class Flow { | |
constructor(dataTable) { | |
this.data = dataTable | |
} | |
_mouvement_avec_category({ depuis, vers, montant, category }) { | |
// sur le sankey, ça se traduit par 2 lignes | |
this.data.addRow([depuis, vers, montant]) | |
this.data.addRow([vers, category, montant]) | |
Bucket.get(depuis).output(montant) | |
Bucket.get(vers).input(montant) | |
Bucket.get(vers).output(montant) | |
Bucket.get(category).input(montant) | |
} | |
_mouvement_sans_category({ depuis, vers, montant }) { | |
this.data.addRow([depuis, vers, montant]) | |
Bucket.get(depuis).output(montant) | |
Bucket.get(vers).input(montant) | |
} | |
revenu({ nature, destination, montant, avec_pct_prelevement_a_la_source = null }) { | |
this._mouvement_sans_category({ depuis: nature, vers: destination, montant }) | |
if (avec_pct_prelevement_a_la_source) { | |
// on ajoute au montant ce qui a déjà été prélevé à la source par les impôts | |
const pourcentage = avec_pct_prelevement_a_la_source | |
const valeur_impot = montant * pourcentage | |
this._mouvement_sans_category({ depuis: nature, vers: "Impôts et taxes", montant: valeur_impot }) | |
} | |
} | |
impot({ nature, depuis, montant }) { | |
this._mouvement_avec_category({ depuis, vers: nature, montant, category: "Impôts et taxes" }) | |
} | |
virement_interne({ nature, depuis, vers, montant }) { | |
// nature ne sert à rien, sauf à documenter le code | |
this._mouvement_sans_category({ depuis, vers, montant }) | |
} | |
epargne({ depuis, vers, montant }) { | |
this._mouvement_avec_category({ depuis, vers, montant, category: "Epargne & Patrimoine" }) | |
} | |
echeance_credit_investissement({ depuis, vers, montant }) { | |
this._mouvement_avec_category({ depuis, vers, montant, category: "Epargne & Patrimoine" }) | |
} | |
echeance_credit_immo({ depuis, vers, montant }) { | |
this._mouvement_avec_category({ depuis, vers, montant, category: "Logement" }) | |
} | |
loyer({ depuis, vers, montant }) { | |
this._mouvement_avec_category({ depuis, vers, montant, category: "Logement" }) | |
} | |
prelevement_automatique({ depuis, nature, montant, category }) { | |
this._mouvement_avec_category({ depuis, vers: nature, montant, category }) | |
} | |
depense({ depuis, nature, montant, category }) { | |
this._mouvement_avec_category({ depuis, vers: nature, montant, category }) | |
} | |
} | |
function drawChart() { | |
var data = new google.visualization.DataTable(); | |
data.addColumn('string', 'Depuis'); | |
data.addColumn('string', 'Vers'); | |
data.addColumn('number', 'Montant'); | |
flow = new Flow(data); | |
////////////////////////////////////////////////////////////////////////////////////// | |
// | |
// | |
// | |
// Décrire ci-dessous les Depenses typiques d'un mois | |
// | |
// | |
// | |
////////////////////////////////////////////////////////////////////////////////////// | |
const CMB_CC = "Compte chèque de Germain" | |
const LIVRET_A_GERMAIN = "Livret A de Germain" | |
const CREDIT_AGRICOLE_CC = "Compte chèque de Jeannine" | |
const ASSURANCE_VIE_JEANNINE = "Assurance vie de Jeannine" | |
const CMB_COMPTE_COMMUN = "Compte commun au CMB avec carte bancaire" | |
const PRET_MAISON = "Remboursement prêt immobilier" | |
//////////// Germain ////////////////////// | |
flow.revenu({ nature: "Salaire de Germain", montant: 1450, destination: CMB_CC, avec_pct_prelevement_a_la_source: 5 / 100 }) | |
flow.revenu({ nature: "Location du garage", montant: 80, destination: CMB_CC }) | |
flow.impot({ nature: "Impôt revenus fonciers", depuis: CMB_CC, montant: 40 / 12 }) | |
flow.impot({ nature: "Prélèvements sociaux sur revenus fonciers", depuis: CMB_CC, montant: 10 / 12 }) | |
flow.virement_interne({ nature: "Contribution de Germain au pôt commun", depuis: CMB_CC, vers: CMB_COMPTE_COMMUN, montant: 800 }) | |
flow.epargne({ depuis: CMB_CC, vers: LIVRET_A_GERMAIN, montant: 50 }) | |
flow.echeance_credit_immo({ depuis: CMB_CC, vers: PRET_MAISON, montant: 700 }) | |
// flow.loyer({ depuis: CMB_CC, vers: "M. Dupont", montant: 500 }) | |
flow.prelevement_automatique({ nature: "CMB frais de tenue de compte", depuis: CMB_CC, montant: 60 / 12, category: "Factures récurrentes" }) | |
//////////// Jeannine ////////////////////// | |
flow.revenu({ nature: "Salaire de Jeannine", destination: CREDIT_AGRICOLE_CC, montant: 1800, avec_pct_prelevement_a_la_source: 13 / 100 }) | |
flow.revenu({ nature: "BlaBlaCar", montant: 80, destination: CREDIT_AGRICOLE_CC }) | |
flow.virement_interne({ nature: "Contribution de Jeannine au pôt commun", depuis: CREDIT_AGRICOLE_CC, vers: CMB_COMPTE_COMMUN, montant: 900 }) | |
flow.epargne({ depuis: CREDIT_AGRICOLE_CC, vers: ASSURANCE_VIE_JEANNINE, montant: 200 }) | |
flow.echeance_credit_immo({ depuis: CREDIT_AGRICOLE_CC, vers: PRET_MAISON, montant: 700 }) | |
flow.prelevement_automatique({ nature: "CA frais de tenue de compte", depuis: CREDIT_AGRICOLE_CC, montant: 40 / 12, category: "Factures récurrentes" }) | |
//////////// Autres ////////////////////// | |
flow.revenu({ nature: "Vente de légumes aux voisins", montant: 20, destination: "Tirelire" }) | |
flow.revenu({ nature: "Vente des conneries du grenier sur Le Bon Coin", montant: 50, destination: "Tirelire" }) | |
flow.impot({ nature: "Taxe d'habitation", depuis: CMB_COMPTE_COMMUN, montant: 1200 / 12 }) | |
flow.impot({ nature: "Contribution à l'audiovisuel public", depuis: CMB_COMPTE_COMMUN, montant: 139 / 12 }) | |
flow.depense({ nature: "Courses", depuis: CMB_COMPTE_COMMUN, montant: 800, category: "dépenses quotidiennes" }) | |
flow.depense({ nature: "Carburant", depuis: CMB_COMPTE_COMMUN, montant: 270, category: "dépenses quotidiennes" }) | |
flow.depense({ nature: "Sorties / Loisirs", depuis: CMB_COMPTE_COMMUN, montant: 150, category: "dépenses quotidiennes" }) | |
flow.prelevement_automatique({ nature: "Free Mobile", depuis: CMB_COMPTE_COMMUN, montant: 19.99, category: "Factures récurrentes" }) | |
flow.prelevement_automatique({ nature: "SFR Mobile", depuis: CMB_COMPTE_COMMUN, montant: 15, category: "Factures récurrentes" }) | |
flow.prelevement_automatique({ nature: "SFR ADSL", depuis: CMB_COMPTE_COMMUN, montant: 15, category: "Factures récurrentes" }) | |
flow.prelevement_automatique({ nature: "gaz", depuis: CMB_COMPTE_COMMUN, montant: 35, category: "Factures récurrentes" }) | |
flow.prelevement_automatique({ nature: "électricité", depuis: CMB_COMPTE_COMMUN, montant: 40, category: "Factures récurrentes" }) | |
flow.prelevement_automatique({ nature: "eau", depuis: CMB_COMPTE_COMMUN, montant: 70, category: "Factures récurrentes" }) | |
flow.prelevement_automatique({ nature: "Canal+ sport", depuis: CMB_COMPTE_COMMUN, montant: 25, category: "Factures récurrentes" }) | |
flow.prelevement_automatique({ nature: "assurance voiture", depuis: CMB_COMPTE_COMMUN, montant: 39.15, category: "Factures récurrentes" }) | |
flow.prelevement_automatique({ nature: "assurance voiture", depuis: CMB_COMPTE_COMMUN, montant: 46.85, category: "Factures récurrentes" }) | |
flow.prelevement_automatique({ nature: "mutuelle santé complémentaire", depuis: CMB_COMPTE_COMMUN, montant: 12.25, category: "Factures récurrentes" }) | |
///////////////////////////////////////////////////////////////////////////////////////////////////////// | |
///////////////////////////////////////////////////////////////////////////////////////////////////////// | |
///////////////////////////////////////////////////////////////////////////////////////////////////////// | |
///////////////////////////////////////////////////////////////////////////////////////////////////////// | |
///////////////////////////////////////////////////////////////////////////////////////////////////////// | |
// Instantiates and draws our chart, passing in some options. | |
var chart = new google.visualization.Sankey(document.getElementById('sankey_basic')); | |
google.visualization.events.addListener(chart, 'error', event => { | |
console.error(event, data) | |
console.dir(data.wg.map(({ c: [{ v: source }, { v: dest }, { v: weight }] }) => ({ source, dest, weight }))) | |
}) | |
chart.draw(data, { | |
width: 1024, | |
sankey: { | |
node: { | |
width: 30, | |
nodePadding: 15, | |
interactivity: true | |
}, | |
iterations: 512 | |
} | |
}); | |
google.visualization.events.addListener(chart, 'select', event => { | |
const selection = chart.getSelection() | |
if (selection.length == 0) { | |
document.getElementById('info').innerHTML = '' | |
} else { | |
const bucket = Bucket.get(selection[0].name) | |
document.getElementById('info').innerHTML = bucket.html | |
} | |
}) | |
google.visualization.events.addListener(chart, 'onmouseover', ({ name }) => { | |
if (name == undefined) { | |
document.getElementById('info').innerHTML = '' | |
} else { | |
const bucket = Bucket.get(name) | |
document.getElementById('info').innerHTML = bucket.html | |
} | |
}) | |
} | |
</script> | |
<style> | |
#info { | |
position: absolute; | |
top: 10px; | |
left: 10px; | |
width: 300px; | |
z-index: 1000; | |
background-color: white; | |
} | |
.bucketInfo { | |
background-color: #6699ee24; | |
padding: 5px; | |
font-size: 10pt; | |
font-family: sans-serif; | |
} | |
.bucket-header { | |
font-size: 14pt; | |
} | |
.bucket-body .positif { | |
color: green; | |
} | |
.bucket-body .negatif { | |
color: darkorange; | |
} | |
</style> | |
</head> | |
<body> | |
<div style="position: relative"> | |
<div id="info" /> | |
</div> | |
<div id="sankey_basic" style="width: 1024px; height: 700px;"></div> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment