Public
Edited
Sep 27, 2023
1 fork
Insert cell
Insert cell
style = html`<style>
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;700&display=swap');

h2 {
font-family:'Roboto', sans-serif;
font-weight:300;
font-size:1em;
fill:#333333;
opacity:0.5;
max-width: 620px;
}

.tooltip{
font-family:'Roboto', sans-serif;
font-size:11.5px;
fill:#333333;
letter-spacing: 0.5px;
text-anchor: start;
}
.leyenda2 {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
font-size: 14px;
text-align: left;
color: #333333;
display: flex;
opacity:1;
margin-block-start: 0;
}

.leyenda3 {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
font-size: 14px;
text-align: left;
color: #333333;
display: flex;
opacity:1;
margin-bottom:30px;
}

.dot_blue {
height: 10px;
width: 10px;
margin-top:3px;
background-color: #31739B;
border-radius: 50%;
display: inline-block;
}

.dot_red {
height: 10px;
width: 10px;
margin-top:3px;
background-color: #F58191;
border-radius: 50%;
display: inline-block;
}

.dot_grey {
height: 10px;
width: 10px;
margin-top:3px;
background-color: #bbb;
border-radius: 50%;
display: inline-block;
}

/* Buscador */
form.oi-ec050e {
flex-wrap: wrap;
width: calc(var(--input-width) + var(--label-width));
max-width: 300px;
}
.oi-ec050e-input {
position: relative;
display: inherit;
background: url("https://static.observableusercontent.com/files/53e10c9e800fb237312516e7243d138d4abe71fa4dab355ce0e4252ab26650b06b7609f28a019f6682062e743b17edb3258b4301b76e512ae826764c4fd855c2")no-repeat 9px 7px;
background-size: 20px;
}

.oi-ec050e-input>input {
box-sizing: border-box;
width: 100%;
height: 32px;
border: 1px solid #121212;
border-radius: 2px;
padding: 2px 8px 2px 50px;
font-weight: 400;
font-size: 15px;
line-height: 1.87;
color: #828282;
background: transparent;
}

input[type=search]::-webkit-calendar-picker-indicator {
display:none !important;
}

.autocomplete-items {
position: absolute;
border: 1px solid #d4d4d4;
border-bottom: none;
border-top: none;
z-index: 99;
/*position the autocomplete items to be the same width as the container:*/
top: 100%;
left: 0;
right: 0;
}

.autocomplete-items div {
padding: 10px;
cursor: pointer;
background-color: #fff;
border-bottom: 1px solid #d4d4d4;
}

/*when hovering an item:*/
.autocomplete-items div:hover {
background-color: #e9e9e9;
}

/*when navigating through the items using the arrow keys:*/
.autocomplete-active {
background-color: DodgerBlue !important;
color: #ffffff;
}

.notita {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
font-size: 10px;
text-align: left;
color: #4F4F4F;
}

.fuente {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
font-size: 12px;
text-align: left;
color: #4F4F4F;
}
</style>`
Insert cell
Insert cell
data = Object.assign(
unemployment,
({
ayuntamiento: ayuntamiento,
año: year,
total_percibido: total_percibido,
ajuste_ipc_b21: ajuste_ipc_b21,
regimen_dedicacion: regimen_dedicacion,
filtro_1: filtro_1
}) => ({
ayuntamiento,
regimen_dedicacion,
year: +year,
total_percibido: +total_percibido,
ajuste_ipc_b21: +ajuste_ipc_b21,
filtro_1: +filtro_1
}),
{}
)
Insert cell
Insert cell
selected_country = selected_country2[0]
Insert cell
municipalites = [...new Set(data.map((item) => item.ayuntamiento))]
Insert cell
municipalites_order2 = municipalites.sort((a, b) => a.length - b.length)
Insert cell
selectionMin = selected_country2.slice(0, 8)
Insert cell
selected_country2
Insert cell
viewof selected_country2 = Inputs.search(data, {
placeholder: "Escriba su población",
datalist: municipalites_order2,
locale: "es",
query: "Madrid",
//format: ??, format - a function to show the number of results.
//format: (result) => `Resultados: ${result / 9}`,
format: (result) => "",
columns: ["ayuntamiento"]
})
Insert cell
Insert cell
chart = LineChart(selectionMin, {
x: (d) => d.año,
y: (d) =>
typeof d.total_percibido === "string"
? +d.total_percibido.replace(",", ".")
: d.total_percibido,
z: (d) => d.ayuntamiento,
tipo: (d) => d.regimen_dedicacion,
ajuste_ipc: (d) => d.ajuste_ipc_b21,
año_ipc: (d) => d.filtro_1,
yLabel: "↑ Sueldo anual alcalde (€)",
width,
height: 600
})
Insert cell
Insert cell
Insert cell
focus = Generators.input(chart) // or say viewof focus = LineChart(…)
Insert cell
// Copyright 2021 Observable, Inc.
// Released under the ISC license.
// https://observablehq.com/@d3/multi-line-chart
function LineChart(
data,
{
x = ([x]) => x, // given d in data, returns the (temporal) x-value
y = ([y]) => y, // given d in data, returns the (quantitative) y-value
z = () => 1, // given d in data, returns the (categorical) z-value
tipo = () => 1,
ajuste_ipc = () => 1,
año_ipc = ([año_ipc]) => año_ipc,
title, // given d in data, returns the title text
defined, // for gaps in data
curve = d3.curveLinear, // method of interpolation between points
marginTop = 20, // top margin, in pixels
marginRight = 30, // right margin, in pixels
marginBottom = 100, // bottom margin, in pixels
marginLeft = 50, // left margin, in pixels
width = 640, // outer width, in pixels
height = 800, // outer height, in pixels
xType = d3.scaleLinear, // type of x-scale
xDomain, // [xmin, xmax]
xRange = [marginLeft, width - marginRight], // [left, right]
yType = d3.scaleLinear, // type of y-scale
yDomain, // [ymin, ymax]
yRange = [height - marginBottom, marginTop], // [bottom, top]
yFormat, // a format specifier string for the y-axis
yLabel, // a label for the y-axis
zDomain, // array of z-values
tDomain, // colores tipo
//color = "#A7C5D8", // stroke color of line, as a constant or a function of *z*
colors = ["#31739B", "#F58191", "#bbb"],
strokeLinecap = "round", // stroke line cap of line
strokeLinejoin, // stroke line join of line
strokeWidth = 2, // stroke width of line
strokeOpacity, // stroke opacity of line
mixBlendMode = "multiply", // blend mode of lines
voronoi // show a Voronoi overlay? (for debugging)
} = {}
) {
// Compute values.
const X = d3.map(data, x);
const Y = d3.map(data, y);
const Z = d3.map(data, z);
const _tipo = d3.map(data, tipo);
const _ajuste_ipc = d3.map(data, ajuste_ipc);
const _año_ipc = d3.map(data, año_ipc);
const O = d3.map(data, (d) => d);
if (defined === undefined) defined = (d, i) => !isNaN(X[i]) && !isNaN(Y[i]);
const D = d3.map(data, defined);

// Compute default domains, and unique the z-domain.
if (xDomain === undefined) xDomain = d3.extent(X);
if (yDomain === undefined)
yDomain = [0, d3.max(Y, (d) => (typeof d === "string" ? +d : d))];
if (zDomain === undefined) zDomain = Z;
zDomain = new d3.InternSet(zDomain);

// Omit any data not present in the z-domain.
const I = d3.range(X.length).filter((i) => zDomain.has(Z[i]));

if (tDomain === undefined) tDomain = _tipo;
tDomain = new d3.InternSet(tDomain);

// Construct scales and axes.
const xScale = xType(xDomain, xRange);
const yScale = yType(yDomain, yRange);
const xAxis = d3
.axisBottom(xScale)
.ticks(width / 150)
.tickSizeOuter(0)
.tickFormat(d3.format(".0f"));
const yAxis = d3
.axisLeft(yScale)
.ticks(height / 60, yFormat)
.tickFormat(function (d) {
return formatNumberES(d, 0);
});
const color = d3.scaleOrdinal(tDomain, colors);

// Compute titles.
const T =
title === undefined ? Z : title === null ? null : d3.map(data, title);

// Construct a line generator sueldo
const line = d3
.line()
.defined((i) => D[i])
.curve(curve)
.x((i) => xScale(X[i]))
.y((i) => yScale(Y[i]));

// Construct a line generator IPC
const line2 = d3
.line()
.defined((i) => D[i])
.curve(curve)
.x((i) => xScale(X[i]))
.y((i) => yScale(_ajuste_ipc[i]));

const svg = d3
.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto; height: intrinsic;")
.style("-webkit-tap-highlight-color", "transparent")
.style("overflow", "visible")
.on("pointerenter", pointerentered)
.on("pointermove", pointermoved)
.on("pointerleave", pointerleft)
.on("touchstart", (event) => event.preventDefault());

svg
.append("g")
.attr("transform", `translate(0,${height - marginBottom})`)
.call(xAxis);

svg
.append("g")
.attr("transform", `translate(${marginLeft},0)`)
.call(yAxis)
.call((g) => g.select(".domain").remove())
.call(
voronoi
? () => {}
: (g) =>
g
.selectAll(".tick line")
.clone()
.attr("x2", width - marginLeft - marginRight)
.attr("stroke-opacity", 0.1)
)
.call((g) =>
g
.append("text")
.attr("x", -marginLeft)
.attr("y", 10)
.attr("fill", "currentColor")
.attr("text-anchor", "start")
.text(yLabel)
);

//**** line y circulos _ajuste_ipc ****//

const path2 = svg
.append("g")
.attr("fill", "none")
.attr("stroke-linecap", strokeLinecap)
.attr("stroke-linejoin", strokeLinejoin)
.attr("stroke-width", strokeWidth)
.attr("stroke-opacity", 0.4)
.selectAll("path")
.data(d3.group(I, (i) => Z[i]))
.join("path")
.style("mix-blend-mode", mixBlendMode)
.attr("stroke", "#F58191")
.attr("d", ([, I]) => line2(I));

svg
.append("g")
.selectAll("circle")
.data(I)
.join("circle")
.attr("cx", (i) => xScale(X[i]))
.attr("cy", (i) => yScale(_ajuste_ipc[i]))
.attr("stroke", "white")
.attr("fill", "#F58191")
.attr("r", (i) => (_ajuste_ipc[i] == null ? 0 : 3));

///**** line y circulos sueldos ****//

const path = svg
.append("g")
.attr("fill", "none")
.attr("stroke-linecap", strokeLinecap)
.attr("stroke-linejoin", strokeLinejoin)
.attr("stroke-width", strokeWidth)
.attr("stroke-opacity", strokeOpacity)
.selectAll("path")
.data(d3.group(I, (i) => Z[i]))
.join("path")
.style("mix-blend-mode", mixBlendMode)
.attr("stroke", "#B7B5A8")
.attr("d", ([, I]) => line(I));

svg
.append("g")
.selectAll("circle")
.data(I)
.join("circle")
.attr("cx", (i) => xScale(X[i]))
.attr("cy", (i) => yScale(Y[i]))
.attr("stroke", "white")
.attr("fill", "grey")
.attr("r", (i) => (Y[i] == null ? 0 : 3));

/*****************************************/
/***** ROLLOVER SUELDO e IPC (dot2) ****/
/*****************************************/

const dot = svg.append("g");
dot.append("circle").attr("r", 2.5).attr("display", "none");

const dot2 = svg.append("g");
dot2.append("circle").attr("r", 2.5).attr("display", "none");

function pointermoved(event) {
const [xm, ym] = d3.pointer(event);
const i = d3.least(I, (i) =>
Math.hypot(xScale(X[i]) - xm, yScale(Y[i]) - ym)
); // closest point

path
.style("stroke", ([z]) => (Z[i] === z ? null : "#efefef"))
.filter(([z]) => Z[i] === z)
.raise();

dot.attr("transform", `translate(${xScale(X[i])},${yScale(Y[i])})`);
dot2.attr(
"transform",
`translate(${xScale(X[i])},${yScale(_ajuste_ipc[i])})`
);

const text = dot.selectAll("text");
if (T)
dot
.select("text")
.attr("font-weight", 400)
.text("En " + X[i] + " el sueldo")
.append("tspan")
.attr("dy", 17)
.attr("x", 0)
.attr("font-weight", 400)
.text("del alcalde de")
.append("tspan")
.attr("dy", 17)
.attr("x", 0)
.attr("font-weight", 700)
.text(T[i])
.append("tspan")
.append("tspan")
.attr("dy", 17)
.attr("x", 0)
.attr("font-weight", 400)
.text("fue de ")
.append("tspan")
.attr("font-weight", 700)
.text(formatNumberES(Y[i]) + "€")
.append("tspan")
.attr("dy", 17)
.attr("x", 0)
.attr("font-weight", 400)
.text(
"(Dedicación: " + (_tipo[i] == null ? "sin info" : _tipo[i]) + ")"
)
.append("tspan")
.attr("dy", 17)
.attr("x", 0)
.text("–––––––––")
.append("tspan")
.attr("dy", 17)
.attr("x", 0)
.text("La proyección del sueldo")
.append("tspan")
.attr("dy", 17)
.attr("x", 0)
.text("para este año")
.append("tspan")
.attr("dy", 17)
.attr("x", 0)
.text(
"respecto al de " + (_año_ipc[i] == null ? "sin info" : _año_ipc[i])
)
.append("tspan")
.attr("dy", 17)
.attr("x", 0)
.attr("font-weight", 700)
.attr("fill", "#F58191")
.text(
"sería de: " +
(_ajuste_ipc[i] == null
? "sin info"
: formatNumberES(_ajuste_ipc[i]) + "€")
);

const path2 = dot
.selectAll("path")
.data([,])
.join("path")
.attr("fill", "white")
.attr("stroke", "black");
dot
.append("text")
.attr("class", "tooltip")
.attr("text-anchor", "end")
.attr("y", -8);

// tooltip positioning
const { x, y, width: w, height: h } = text.node().getBBox();
if (X[i] < 2014) {
text.attr("transform", `translate(${-w / 2 + w / 1.3},${y})`);
} else if (X[i] > 2019) {
text.attr("transform", `translate(${-w / 2 - w / 1.3},${y - 30})`);
} else {
text.attr("transform", `translate(${-w / 2},${15 - y})`);
}

// tooltip container path

if (X[i] < 2014) {
path2.attr(
"d",
`M ${x + 20},${y - h / 2 + 50} L ${x + w + 60},${y - h / 2 + 50} L ${
x + w + 60
},${y + h} L ${x + 20},${y + h} L ${x + 20},${y - h / 2 + 50}`
);
} else if (X[i] > 2019) {
path2.attr(
"d",
`M ${x - 30},${y - h / 2 + 20} L ${x - w - 60},${y - h / 2 + 20} L ${
x - w - 60
},${y + h - 30} L ${x - 30},${y + h - 30} L ${x - 30},${y - h / 2 + 20}`
);
} else {
path2.attr(
"d",
`M${-w / 2 - 10},5H-5l5,-5l5,5H${w / 2 + 10}v${h + 20}h-${w + 20}z`
);
}
}

function pointerentered() {
path.style("mix-blend-mode", null).style("stroke", "#ddd");
dot.attr("display", true);
}

function pointerleft() {
path.style("mix-blend-mode", mixBlendMode).style("stroke", null);
dot.attr("display", "none");
svg.node().value = null;
svg.dispatch("input", { bubbles: true });
}

return Object.assign(svg.node(), { value: null });
}
Insert cell
/*
* Funcion para devolver un numero formateado con separadores de miles
* y decimales en formato español
* @param {int|float|string} n - numero valido en formato entero, float o string
* @param {int} d - numero de decimales
*/
formatNumberES = (n, d = 1) => {
n = new Intl.NumberFormat("es-ES").format(parseFloat(n).toFixed(d));
if (d > 0) {
// Obtenemos la cantidad de decimales que tiene el numero
const decimals = n.indexOf(",") > -1 ? n.length - 1 - n.indexOf(",") : 0;

// añadimos los ceros necesios al numero
n = decimals == 0 ? n + "," + "0".repeat(d) : n + "0".repeat(d - decimals);
}
return n;
}
Insert cell
formatNumberES(10000.35)
Insert cell
import {howto, altplot} from "@d3/example-components"
Insert cell
import { submit } from "@mbostock/more-deliberate-inputs"
Insert cell
lupa = FileAttachment("lupa.svg").image()
Insert cell
FileAttachment("lupa.svg").url()
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more