Skip to content

Instantly share code, notes, and snippets.

@ManuelBlanc
Last active January 16, 2017 20:26
Show Gist options
  • Save ManuelBlanc/7cf0a0f21a7d7c7dd3b09767ef895c93 to your computer and use it in GitHub Desktop.
Save ManuelBlanc/7cf0a0f21a7d7c7dd3b09767ef895c93 to your computer and use it in GitHub Desktop.
license: CC-BY-SA-4.0
(function(window) { "use strict";
// Locale español
d3.formatDefaultLocale({
"decimal": ",",
"thousands": ".",
"grouping": [3],
"currency": ["€", ""],
"dateTime": "%a %b %e %X %Y",
"date": "%d/%m/%Y",
"time": "%H:%M:%S",
"periods": ["AM", "PM"],
"days": ["Domingo", "Lunes", "Martes", "Miércoles", "Jueves", "Viernes", "Sábado"],
"shortDays": ["Dom", "Lun", "Mar", "Mie", "Jue", "Vie", "Sab"],
"months": ["Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"],
"shortMonths": ["Ene", "Feb", "Mar", "Abr", "May", "Jun", "Jul", "Ago", "Sep", "Oct", "Nov", "Dic"]
});
// Accesor generico
window.getter = function(keys, transform) {
return function (obj) {
if (Array.isArray(keys)) {
keys.every(function (key) {
return (obj = obj[key]);
});
}
else {
obj = obj[keys];
}
return (transform !== undefined) ? transform(obj) : obj;
}
};
window.schemeFromInterpolator = function(n, interpolator, scale) {
var domain = ticks(n);
if (scale !== undefined) {
if (Array.isArray(scale)) {
domain = domain.map(d3.scaleLinear().range(scale));
}
else {
domain = scale(domain);
}
}
return domain.map(interpolator);
}
window.ticks = function(n) {
return d3.range(n--).map(function(k) { return k/n; });
};
})(window);
(function() { "use strict";
var svg = d3.select("svg#bars"),
margin = {top: 20, right: 100, bottom: 50, left: 100},
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom,
g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// Generacion de datos
var generateData = function(p, i, n) {
var A = [];
var c = calculateC(p, i, n)
d3.range(n).forEach(function(k) {
var ci = p*i;
var ca = c - ci;
A.push({
"k": k+1, // Plazo
"i": ci, // Intereses
"a": ca, // Amortizado
"c": c, // Cuota (es fija)
"p": p - ca, // Principal restante
"t": p + ci // Total
});
p = p * (1+i) - c;
});
return A;
function calculateC(p, i, n) {
return (i != 0) ? p * i/(1 - Math.pow(1+i, -n)) : p/n;
}
}
var data = null;
// Escalas
var x = d3.scaleBand()
.rangeRound([0, width])
.paddingOuter(0.1)
.paddingInner(0.05);
var y = d3.scaleLinear()
.rangeRound([height, 0]);
var z = d3.scaleOrdinal()
.range(d3.schemeCategory20b);
// Formateadores
var xFmt = d3.format("i");
var yFmt = d3.format("$.2f");
// Eje x
g.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + height + ")")
.append("text")
.attr("class", "label")
.attr("x", width/2)
.attr("y", margin.bottom*0.75)
.style("text-anchor", "middle")
.attr("alignment-baseline", "central")
.text("Plazo (k)");
// Eje y
g.append("g")
.attr("class", "axis axis--y")
.append("text")
.attr("class", "label")
.attr("transform", "rotate(-90)")
.attr("x", -(height / 2))
.attr("y", 10-margin.left)
.attr("dy", "1em")
.style("text-anchor", "middle")
.text("Cantidad (€)");
var barGroup = g.append("g");
var line = g.append("g").attr("class", "line");
line.append("line")
.attr("x1", 2)
.attr("x2", 100)
.style("stroke-width", "2px")
.style("stroke", "maroon")
.style("opacity", "0.75");
line.append("text")
.attr("text-anchor", "end")
.attr("x", 100)
.attr("dy", "-0.4em")
.style("fill", "maroon")
.text("-,--€");
var legend = g.append("g")
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.attr("text-anchor", "end")
.selectAll("g")
.data(["Pendiente", "Amortizado", "Intereses"])
.enter().append("g")
.attr("transform", function(d, i) { return "translate(" + margin.right + "," + (20+i*20) + ")"; });
legend.append("rect")
.attr("x", width - 19)
.attr("width", 19)
.attr("height", 19)
.attr("fill", z);
legend.append("text")
.attr("x", width - 24)
.attr("y", 9.5)
.attr("dy", "0.32em")
.text(function(d) { return d; });
var copyKey = function(series) {
series.forEach(function(d) {
d.key = series.key;
d.index = series.index;
});
return series;
}
var slider_i = d3.select("#bar_i input").on("input", update);
var slider_n = d3.select("#bar_n input").on("input", update);
update();
function update() {
var i = +slider_i.property("value");
var n = +slider_n.property("value");
data = generateData(1000, i, n);
d3.select("#bar_i span").text(d3.format(".2%")(i));
d3.select("#bar_n span").text(xFmt(n));
// Actualizamos las escalas
x.domain(d3.range(1, +slider_n.property("max")+1));
y.domain([0, (+slider_i.property("max")+1)*1000]);
//x.domain(data.map(getter("k")));
//y.domain([0, d3.max(data, function(d) { return d.c + d.p; })]);
z.domain(["p", "a", "i"]);
// Los ejes
g.select(".axis--x").call(d3.axisBottom(x).tickFormat(xFmt))
g.select(".axis--y").call(d3.axisLeft(y).tickFormat(yFmt))
// La linea
line.attr("transform", "translate(" +
(x(n)+x.bandwidth()) + "," + y(data[0].c) + ")"
);
line.select("text").text(yFmt(data[0].c));
// JOIN - Union con los datos nuevos
var bar = barGroup.selectAll(".bar")
.data(
d3.zip.apply(null, d3.stack().keys(["i", "a", "p"])(data).map(copyKey))
);
// EXIT - Borramos los viejos
bar.exit().remove();
// ENTER - Insertamos elementos nuevos
var barEnter = bar.enter().append("g").attr("class", "bar");
barEnter.append("text");
// UPDATE - Actualizamos los datos
var barUpdate = bar.merge(barEnter);
var barUpdateRects = barUpdate.selectAll("rect")
.data(function(d) { return d; });
barUpdateRects.exit().remove();
barUpdateRects.enter().append("rect").merge(barUpdateRects)
.attr("fill", getter("key", z))
.attr("x", getter(["data", "k"], x))
.attr("y", getter(1, y))
.attr("height", function(d) { return y(d[0]) - y(d[1]); })
.attr("width", x.bandwidth());
barUpdate.select("text")
.attr("x", getter([0, "data", "k"], x))
.attr("y", getter([0, "data", "t"], y))
.attr("dx", 5)
.attr("dy", "-0.3em")
.style("text-anchor", "begin")
.text(getter([0, "data", "t"], yFmt));
// .text(function(d) {
// d = d[0].data;
// var t = yFmt(d.t);
// var p = yFmt(d.p);
// var a = yFmt(d.a);
// var i = yFmt(d.i);
// return t + "\n" + p + "\n" + a + "\n" + i;
// });
}
})();
(function(window) { "use strict";
// Margenes estandar
var svg = d3.select("svg#graph"),
margin = {top: 20, right: 20, bottom: 50, left: 60},
width = svg.attr("width") - margin.left - margin.right,
height = svg.attr("height") - margin.top - margin.bottom;
// Elemento raiz
var g = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// Generacion de datos
var data = (function() {
var A = [];
d3.range(12, 49, 3).forEach(function(n) {
d3.range(0, 0.21, 0.01).forEach(function(i) {
var c = calculateC(100, i, n);
A.push({
"i": i,
"n": n,
"c": c,
"p": n*c - 100
});
});
});
return A;
function calculateC(p, i, n) {
return (i != 0) ? p * i/(1 - Math.pow(1+i, -n)) : p/n;
}
})();
// Escalas
var x = d3.scaleBand().range([0, width]);
var y = d3.scaleBand().range([height, 0]);
var z = (function() {
var shadingEnabled = true;
var binCount, colorScale = d3.scaleThreshold();
var scale = d3.scaleSequential(function(t) {
var color = colorScale(t);
if (shadingEnabled) {
var r = (t !== 1) ? t * binCount % 1 : 1;
return color.darker(r*0.4);
}
return color;
});
scale.colorScheme = function(colors, disableShading) {
shadingEnabled = !disableShading;
binCount = colors.length;
colorScale
.domain(ticks(1+binCount).slice(1, -1))
.range(colors.map(d3.color));
return scale;
};
scale.colorScheme(d3.schemeSet1.slice(0, 5));
return scale;
})();
// Formateadores
var xFmt = d3.format(".0%");
var yFmt = d3.format("i");
var zFmt = d3.format("$.0f");
// Eje x
g.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + height + ")")
.append("text")
.attr("class", "label")
.attr("x", width/2)
.attr("y", margin.bottom*0.75)
.style("text-anchor", "middle")
.attr("alignment-baseline", "central")
.text("Intereses (i)");
// Eje y
g.append("g")
.attr("class", "axis axis--y")
.append("text")
.attr("class", "label")
.attr("transform", "rotate(-90)")
.attr("x", -(height / 2))
.attr("y", 10-margin.left)
.attr("dy", "1em")
.style("text-anchor", "middle")
.text("Plazos (n)");
var infoLabel = g.append("text")
.attr("class", "info")
.attr("x", width)
.attr("dx", "-0.5em")
.attr("dy", "-0.5em")
.style("text-anchor", "end")
.text("<selecciona una celda>");
svg.on("mouseout", function() {
infoLabel.text("<selecciona una celda>");
})
var radio = d3.selectAll("#key input[type='radio']").on("change", update);
update();
var key = "c";
function update() {
key = radio.filter(":checked").property("value");
// Actualizamos las escalas
x.domain(data.map(getter("i")));
y.domain(data.map(getter("n")).reverse());
z.domain(d3.extent(data, getter(key)));
if (key == "c") {
z.colorScheme(schemeFromInterpolator(7, d3.interpolateViridis, [0, 0.7]));
}
else {
z.colorScheme(schemeFromInterpolator(6, d3.interpolatePlasma, [0, 0.7]));
}
// Los ejes
g.select(".axis--x").call(d3.axisBottom(x).tickFormat(xFmt))
g.select(".axis--y").call(d3.axisLeft(y).tickFormat(yFmt))
// JOIN - Union con los datos nuevos
var cell = g.selectAll(".cell").data(data);
// EXIT - Borramos los viejos (no se usa!)
cell.exit().remove();
// ENTER - Insertamos elementos nuevos
var cellEnter = cell.enter()
.append("g")
.attr("class", "cell");
cellEnter.append("rect");
cellEnter.append("text")
.attr("text-anchor", "middle")
.attr("alignment-baseline", "central");
cellEnter.on("mouseover", function(d) {
infoLabel.text(d3.format("$.2f")(d[key]));
});
// UPDATE - Actualizamos los datos
var cellUpdate = cell.merge(cellEnter);
cellUpdate.select("rect") // relleno
.attr("x", getter("i", x))
.attr("y", getter("n", y))
.attr("width", x.bandwidth())
.attr("height", y.bandwidth())
.style("fill", getter(key, z));
cellUpdate.select("text") // etiqueta
.attr("x", getter("i", x))
.attr("y", getter("n", y))
.attr("dx", 0.5*x.bandwidth())
.attr("dy", 0.5*y.bandwidth())
.text(getter(key, zFmt));
}
})(window);
<!DOCTYPE html>
<meta charset='utf-8'>
<meta http-equiv='X-UA-Compatible' content='IE=edge'>
<title>Visualizacion grafica: prestamos</title>
<link rel='stylesheet' href='style.css'>
<script defer src='//d3js.org/d3.v4.min.js'></script>
<script defer src='//d3js.org/d3-scale-chromatic.v1.min.js'></script>
<script async src='//cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-MML-AM_CHTML'></script>
<script defer src='common.js'></script>
<script defer src='graph_contour.js'></script>
<script defer src='graph_bars.js'></script>
<h1>Visualizacion grafica: prestamos</h1>
<h2>Introduccion: que es un prestamo?</h2>
<p>Un <strong>prestamo</strong> es un contrato en el que una entidad presta dinero a otra. La entidad que recibe el dinero se denomina <em>prestatario</em> y el que la otorga se llama <em>prestamista</em>. El prestamo tiene un coste para el prestatario en la forma de unos intereses sobre la deuda. Los prestamos son beneficiosos para ambas partes:
<ul>
<li>El <em>prestamista</em> genera rendimientos de un capital que estaria inmovil y devaluandose por la inflacion.
<li>El <em>prestatario</em> puede afrontar pagos de inmediato en vez de tener que ahorrar.</li>
</ul>
En los dos casos se evita que haya dinero inactivo.</p>
<p>Los reembolsos se realizan en plazos. Los dos sistemas mas comunes son:
<ul>
<li><strong>Sistema americano</strong>: durante la vida del prestamo solo se pagan los intereses. En el ultimo reembolso, se devuelva la totalidad del principal.</li>
<li><strong>Sistema frances</strong>: se paga la misma cuota todos los plazos.</li>
</ul>
El resto del articulo se centra en el sistema frances.
Para calcular la cuota en este sistema, se usa la siguiente formula:
\[ c = \frac{p\cdot{}i}{1-(1+i)^{-n}} \]
</p>
Donde:
<dl>
<dt>\(c\)</dt>
<dd>Cuota a pagar en cada plazo</dd>
<dt>\(p\)</dt>
<dd>Cantidad prestada (principal)</dd>
<dt>\(i\)</dt>
<dd>Intereses</dd>
<dt>\(n\)</dt>
<dd>Numero de plazos</dd>
</dl>
<em>(Fuente: notas de clase, paginas web varias)</em>
<h2>Ejemplo interactivo: plan de amortizaciones para un prestamo de 100€</h2>
<form>
<label id='bar_i'><input type='range' min='0' max='0.2' step='0.01' value='0.15'>Intereses <span>0</span></label>
<label id='bar_n'><input type='range' min='12' max='46' step='1' value='24'>Plazos <span>0</span></label>
</form>
<svg id='bars' width='960' height='500'></svg>
<br/>
<p>(<strong>Ayuda:</strong> Usa los deslizadores de intereres y plazos encima de la grafica para ver su efecto sobre el plan.)</p>
<h2>Ejemplo interactivo: efecto del interes y plazo sobre la cuota</h2>
<form id='key'>
<label><input type='radio' name='key' value='c' checked>Cuota (\(c\))</label>
<label><input type='radio' name='key' value='p'>Intereses totales (\(I\))</label>
</form>
<svg id='graph' width='960' height='500'></svg>
<br/>
<p>Esta tabla muestra varias cuotas y como varia la cuota con varios plazos e intereses. Se ha coloreado en funcion del valor de \(c\) de manera discreta que se vea una aproximacion de las curvas de nivel (aquellas en las que la \(c\) se mantiene constante). Se han colocado los ejes de manera que se lea de la siguiente manera:
<ul>
<li>Busca la columna que tenga el tipo de interes que ofrezca el prestatario.</li>
<li>Lee de arriba-a-abajo hasta encontrar la cuota mas elevada que estes dispuesto a pagar.</li>
</ul>
Las cantidades estan redondeadas para ahorrar espacio. Pon el cursor encima para mostrar la cantidad exacta.</p>
<h2>Tasa Anual Equivalente (TAE)</h2>
<p>Cuando el periodo de tiempo previsto para el calculo y liquidacion de intereses coincide con la forma de expresión del tipo de interes se esta utilizando un tipo de interes nominal. Para poder comparar prestamos en las que la periodicidad de las liquidaciones es diferente, se usa la TAE (\(r\)):
\[ ( 1 + r ) = \left( 1 + \frac{i}{n} \right) ^ n \]
Esta formula nos dice que el tipo de interes nominal \(i\) para \(n\) plazos equivale a un tipo de interes anual de \(r\).
</p>
<!--
<ul>
<li></li>
</ul>
\(0.7974\%\) effective monthly interest rate, because 1.00797412=1.1
\(9.569\%\) annual interest rate compounded monthly, because 12×0.7974=9.569
\(9.091\%\) annual rate in advance, because (1.1-1)÷1.1=0.09091
-->
<h3>Ley Europea al rescate: TAE representativo</h3>
<p>Para simplificar la comparacion de prestamos, se aprobo una directiva Europea que define una manera estandar de calcular la TAE que tiene en cuenta tasas adicionales y normaliza el tipo de interes. Las entidades que ofrezcan creditos al consumo deben informar a sus clientes de esta TAE representativa. La formulada usada es la siguiente:</p>
\[ \sum_{l=0}^M S_l (1 + \hat{r})^{-t_l} = \sum_{k=0}^N A_k (1 + \hat{r})^{-t_k} \]
Esta explicada con detalle en la Wikipedia: <a href='https://en.wikipedia.org/wiki/Annual_percentage_rate#European_Union'>Annual Percentage Rate</a> (ingles). Simplificando, \(\hat{r}\) es el tipo de interes con el cual el total de cash-flows en ambas direcciones (cobros+pagos) sea igual de acuerdo con la matematica financiera.
<h1>Creditos</h1>
<ul>
<li>Escrito y programado por <a href='https://github.com/ManuelBlanc'>ManuelBlanc</a> (codigo fuente disponible en <a href='https://gist.github.com/ManuelBlanc/7cf0a0f21a7d7c7dd3b09767ef895c93/'>GitHub</a>).</li>
<li>Graficos elaborados con D3.js (<a href='https://d3js.org'>https://d3js.org</a>).</li>
<li>Usa esquemas de colores de <a href='http://colorbrewer2.org'>http://colorbrewer2.org</a>.</li>
</ul>
<br/>
<a rel="license" href="http://creativecommons.org/licenses/by-sa/4.0/"><img alt="Licencia de Creative Commons" style="border-width:0" src="https://i.creativecommons.org/l/by-sa/4.0/88x31.png" /></a><br/>Este obra está bajo una <a rel="license" href="http://creativecommons.org/licenses/by-sa/4.0/">licencia de Creative Commons Reconocimiento-CompartirIgual 4.0 Internacional</a>
body {
width: 960px;
margin-bottom: 4em;
font-size: 1em;
font-family: Verdana, Arial, sans-serif;
}
svg {
shape-rendering: crispEdges;
}
.cell:hover rect, .bar:hover rect {
opacity: 0.7;
}
.cell text {
font: 12px Arial;
fill: #eee;
pointer-events: none;
}
.info {
font: 15px Verdana;
font-weight: bold;
}
.axis .label {
fill: black;
font-size: 1.5em;
}
.bar text {
font: 14px Arial;
fill: black;
font-weight: 800;
opacity: 0;
}
.bar:hover text {
opacity: 1;
}
label span {
font-family: monospace;
}
form {
text-align: center;
}
label {
display: inline-block; padding-left: 1em;
width: 300px;
}
input[type='radio'] {
vertical-align: middle;
}
input[type='range'] {
width: 100px;
margin-right: 1em;
}
/*
font-family: 'Latin Modern Math', MathJax_Main, STIXGeneral, STIXSizeOneSym, 'DejaVu Sans', 'DejaVu Serif', Cambria, 'Cambria Math', 'Lucida Sans Unicode', 'Arial Unicode MS', 'Lucida Grande', OpenSymbol, 'Standard Symbols L', Times, serif;
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment