Public
Edited
Feb 29, 2024
Importers
Insert cell
Insert cell
Insert cell
// Copyright 2022 Observable, Inc.
// Released under the ISC license.
// https://observablehq.com/@d3/bubble-map
function BubbleMap(data, {
position = d => d, // given d in data, returns the [longitude, latitude]
value = () => undefined, // given d in data, returns the quantitative value
title, // given a datum d, returns the hover text
scale = d3.scaleSqrt, // type of radius scale
domain, // [0, max] values; input of radius scale; must start at zero
maxRadius = 40, // maximum radius of bubbles
width = 640, // outer width, in pixels
height, // outer height, in pixels
projection, // a D3 projection; null for pre-projected geometry
features, // a GeoJSON feature collection for the background
borders, // a GeoJSON object for stroking borders
outline = projection && projection.rotate ? {type: "Sphere"} : null, // a GeoJSON object for the background
backgroundFill = "#ededed", // fill color for background
backgroundStroke = "white", // stroke color for borders
backgroundStrokeWidth, // stroke width for borders
backgroundStrokeOpacity, // stroke width for borders
backgroundStrokeLinecap = "round", // stroke line cap for borders
backgroundStrokeLinejoin = "round", // stroke line join for borders
fill = "brown", // fill color for bubbles
fillOpacity = 0.5, // fill opacity for bubbles
stroke = "white", // stroke color for bubbles
strokeWidth = 0.5, // stroke width for bubbles
strokeOpacity, // stroke opacity for bubbles
legendX = width - maxRadius - 10,
legendY = height - 10,
} = {}) {

// Compute values.
const I = d3.map(data, (_, i) => i);
const V = d3.map(data, value).map(d => d == null ? NaN : +d);
const P = d3.map(data, position);
const T = title == null ? null : d3.map(data, title);

// Compute default domains.
if (domain === undefined) domain = [0, d3.max(V)];

// Construct scales.
const radius = scale(domain, [0, maxRadius]);

// Compute the default height. If an outline object is specified, scale the projection to fit
// the width, and then compute the corresponding height.
if (height === undefined) {
if (outline === undefined) {
height = 400;
} else {
const [[x0, y0], [x1, y1]] = d3.geoPath(projection.fitWidth(width, outline)).bounds(outline);
const dy = Math.ceil(y1 - y0), l = Math.min(Math.ceil(x1 - x0), dy);
projection.scale(projection.scale() * (l - 1) / l).precision(0.2);
height = dy;
}
}

// Construct a path generator.
const path = d3.geoPath(projection);

const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "width: 100%; height: auto; height: intrinsic;");

if (outline != null) svg.append("path")
.attr("fill", "white")
.attr("stroke", "currentColor")
.attr("d", path(outline));

svg.append("path")
.datum(features)
.attr("fill", backgroundFill)
.attr("d", path);

if (borders != null) svg.append("path")
.attr("pointer-events", "none")
.attr("fill", "none")
.attr("stroke", backgroundStroke)
.attr("stroke-linecap", backgroundStrokeLinecap)
.attr("stroke-linejoin", backgroundStrokeLinejoin)
.attr("stroke-width", backgroundStrokeWidth)
.attr("stroke-opacity", backgroundStrokeOpacity)
.attr("d", path(borders));

const legend = svg.append("g")
.attr("fill", "#777")
.attr("transform", `translate(${legendX},${legendY})`)
.attr("text-anchor", "middle")
.style("font", "10px sans-serif")
.selectAll("g")
.data(radius.ticks(4).slice(1))
.join("g");

legend.append("circle")
.attr("fill", "none")
.attr("stroke", "#ccc")
.attr("cy", d => -radius(d))
.attr("r", radius);

legend.append("text")
.attr("y", d => -2 * radius(d))
.attr("dy", "1.3em")
.text(radius.tickFormat(4, "s"));

svg.append("g")
.attr("fill", fill)
.attr("fill-opacity", fillOpacity)
.attr("stroke", stroke)
.attr("stroke-width", strokeWidth)
.attr("stroke-opacity", strokeOpacity)
.selectAll("circle")
.data(d3.range(data.length)
.filter(i => P[i])
.sort((i, j) => d3.descending(V[i], V[j])))
.join("circle")
.attr("transform", projection == null
? i => `translate(${P[i]})`
: i => `translate(${projection(P[i])})`)
.attr("r", i => radius(V[i]))
.call(T ? circle => circle.append("title").text(i => T[i]) : () => {});

return Object.assign(svg.node(), {scales: {radius}});
}
Insert cell
Insert cell
base_topos = FileAttachment("base.R.topo(1).json").json()
Insert cell
regionesMap = new Map(topojson.feature(base_topos, base_topos.objects.base).features.map(d => [d.properties.territoryKey, d]))
Insert cell
desercion_pais2 = FileAttachment("desercion_pais2.json").json()
Insert cell
Insert cell
viewof añoDesercion = slider({
title: "Año",
min: 2012,
max: 2021,
step: 1,
value: 2012,
background: {
type: "double",
colors: ["#A30010", "#EDEDED"]
}
})

//año_opciones
Insert cell
viewof nivel = select({
title: "Rango etario",
options: nivel_opciones,
selectStyle: {
background: "#EDEDED"
}
})
Insert cell
nivel_opciones = [...new Set(desercion_pais2.map(data => data[1]))]
Insert cell
desercion_pais2
Insert cell
Insert cell
desercionFiltrada2 = desercion_pais2.filter(data => (data[0] === añoDesercion) & (data[1] === nivel))
Insert cell
desercion_pais2
Insert cell
//selector_desercion = selectDivDesercion()
Insert cell
chart3 = BubbleMap(desercionFiltrada2,
{

value: ([, , , , tasa, ,]) => tasa,
position([, , , , , ,territoryKey]) {
const region = regionesMap.get(territoryKey);
return region && centroid(region);
},
title([, , , , , ,territoryKey]) {
const region = regionesMap.get(territoryKey);
return `${region?.properties.region}\n${(+desercionFiltrada2).toLocaleString("en")}`;
},
maxRadius: 20, // maximum radius of bubbles
width: 140, // outer width, in pixels
height:100, // outer height, in pixels
features: geoObj_region,
projection: projection_region(geoObj_region),
borders: regionMesh,
width: 975,
height: 610
})
Insert cell
regionMesh = topojson.mesh(base_topos, base_topos.objects.base, (a, b) => a !== b)
Insert cell
getMapConf_region("Pais")
Insert cell
base_topos.objects
Insert cell
geoObj_region = getGeoObj_region('Pais');
Insert cell
getGeoObj_region = (mapType) => {
const topo = topos_region[mapType];
return {
type: 'GeometryCollection',
geometries: topo.geometries.map(region => {
const mesh = topojson.mesh(base_topos, region)
return {
...mesh,
properties: {
territoryKey: region.properties.territoryKey,
centroid: d3.geoCentroid(mesh),
region: region.properties.region
}
}
})
}
}
Insert cell
topos_region = ({
'Pais': filterTopologia_region('Pais', regionesPorPais.Pais),
})
Insert cell
regionesPorPais = ({'Pais': ['m.2021.t.r.3006',
'm.2021.t.r.3009',
'm.2021.t.r.3013',
'm.2021.t.r.3010',
'm.2021.t.r.3014',
'm.2021.t.r.3007',
'm.2021.t.r.3004',
'm.2021.t.r.3012',
'm.2021.t.r.3005',
'm.2021.t.r.3008',
'm.2021.t.r.3011',
'm.2021.t.r.3001',
'm.2021.t.r.3015',
'm.2021.t.r.3002',
'm.2021.t.r.3016',
'm.2021.t.r.3003']})
Insert cell
filterTopologia_region = (filter, distList) => {
const geometries = base_topos.objects.base.geometries
if (filter === "Pais"){
return {
type: 'GeometryCollection',
geometries: geometries.filter(
geom => distList.includes(geom.properties.territoryKey)),
extra: true
}
}
}
Insert cell
getMapConf_region = (mapType) => {
const geoObj_region = getGeoObj_region(mapType);
const mapProjection_region = projection_region(geoObj_region);
return {
geoObj: geoObj_region,
mapProjection: mapProjection_region
}
}
Insert cell
projection_region = (geom) => {
return d3.geoMercator().fitSize([width, height], geom)
}
Insert cell
height = 600
Insert cell
width = 600
Insert cell
import { slider } from "@bartok32/diy-inputs"
Insert cell
import { get_secuential_colors, get_colors, fonts, colors, legend } from "981f894a8cb8875d"
Insert cell

<style>
@import url('${fonts['condensed']['url']}');
/* Grid container select */

.item3 {
grid-area: tipomapa;
min-width: 0;
min-height: 0;
}

.item4 {
grid-area: seleccion;
min-width: 0;
min-height: 0;
}

.item5 {
grid-area: genero;
min-width: 0;
min-height: 0;
}

.item6 {
grid-area: ano;
min-width: 0;
min-height: 0;
}

.grid-container-select {
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
grid-gap: 20px;
}

/* Mapa */

.decideChileMap {
stroke:#FFFFFF;
stroke-width: 0.2;
stroke-linejoin: round;
}

.map-container {
max-height: 200px;
}

/* Tooltip */

.arrow_box {
pointer-events: none;
font-family: ${fonts['default']['family']};
font-size: 13px;
padding: 8px 15px;
position: absolute;
background: #ffffff;
border-radius: 2px;
box-shadow: 0 10px 16px 0 rgba(0,0,0,0.2),8px 6px 15px 0 rgba(0,0,0,0.19);
display: flex;
flex-direction: column;
}

strong {
border-bottom: 1px solid black;
font-family: ${fonts['default']['family']};
padding-bottom: 2px;
justify-content: center;
display: flex;
color: #1A1A1A;
font-weight: 900;
font-size: 13px;
}

.texto-subtitulo {
font-family: ${fonts['condensed']['family']};
color: #1A1A1A;
font-weight: 400;
font-size: 10px;
justify-content: center;
display: flex;
}

.texto-primario {
color: #1A1A1A;
font-family: ${fonts['default']['family']};
font-weight: 700;
font-size: 12px;
padding-top: 3px;
justify-content: center;
display: flex;
}

.texto-sin-informacion {
color: #1A1A1A;
font-family: ${fonts['condensed']['family']};
font-weight: 450;
font-size: 12px;
padding-top: 3px;
justify-content: center;
display: flex;
}

.texto-secundario {
color: #1A1A1A;
font-family: ${fonts['condensed']['family']};
font-size: 10px;
font-weight: 400;
justify-content: center;
display: flex;
}

.tooltip {
color: #1A1A1A;
font-family: ${fonts['condensed']['family']};
font-size: 10px;
font-weight: 700;
justify-content: center;
display: flex;
}
</style>

Insert cell
import { Legend, Swatches } from "@d3/color-legend"
Insert cell
desercion_pais_llaves = FileAttachment("desercion_pais_llaves.json").json()
Insert cell
desercion_filtrada_llaves = desercion_pais_llaves.filter(data => (data.año === añoDesercion) & (data.nivel === nivel))
Insert cell
extent_region = d3.extent(desercion_pais2, (d) => +d[3])
Insert cell
colorScale_region = d3
.scaleThreshold()
.domain([0, 500, 1000, 1500, 2000, 2500, Math.ceil(extent_region[1])])
.range(colores_region)
Insert cell
colores = d3.quantize(d3.interpolateViridis, 7)
Insert cell
colores_region = ["#ffffb2","#fed976","#feb24c","#fd8d3c","#fc4e2a","#e31a1c","#b10026"]
Insert cell
Insert cell
import {howto} from "@d3/example-components"
Insert cell
Insert cell
selector_desercion2 = selectDivDesercion()
Insert cell
viewof bubbleMap_desercion_regiones = BivariateBubbleMap(desercion_filtrada_llaves, {
nameValue: ({ region }) => region,
sizeValue: ({ value }) => +value,
colorValue: ({ desertores }) => +desertores,
territory: ({territoryKey}) => territoryKey,
position({territoryKey}) {
const region = regionesMap.get(territoryKey);
return region && centroid(region);
},
title({territoryKey}) {
const region = regionesMap.get(territoryKey);
return `${region?.properties.region}\n${(+desercion_filtrada_llaves).toLocaleString("en")}`;
},
colorScale: colorScale_region,
maxRadius: 20, // maximum radius of bubbles
width: 140, // outer width, in pixels
height:100, // outer height, in pixels
features: geoObj_region,
projection: projection_region(geoObj_region),
borders: regionMesh,
width: 975,
height: 610,
legendColorTitle: "Cantidad de desertores",
legendBubbleTitle: "Tasa de Incidencia",
createTooltip : createTooltip
})
Insert cell
BivariateBubbleMap = function BivariateBubbleMap(data,
{
position = (e) => e, // given d in data, returns the [longitude, latitude]
sizeValue = () => undefined, // given d in data, returns the quantitative value
nameValue = () => undefined,
colorValue = () => undefined, // given d in data, returns the value to use for the fill and stroke
territory = () => undefined,
title, // given a datum d, returns the hover text
scale = d3.scaleSqrt, // type of radius scale
domain, // [0, max] values; input of radius scale; must start at zero
maxRadius = 15, // maximum radius of bubbles
width = 640, // outer width, in pixels
height , // outer height, in pixels
projection, // a D3 projection; null for pre-projected geometry
features, // a GeoJSON feature collection for the background
borders, // a GeoJSON object for stroking borders
outline = projection && projection.rotate ? { type: "Sphere" } : null, // a GeoJSON object for the background
backgroundFill = "#ededed", // fill color for background
backgroundStroke = "white", // stroke color for borders
backgroundStrokeWidth, // stroke width for borders
backgroundStrokeOpacity, // stroke width for borders
backgroundStrokeLinecap = "round", // stroke line cap for borders
backgroundStrokeLinejoin = "round", // stroke line join for borders
colorScale = d3.scaleLinear(d3.interpolateViridis),
fillOpacity = 0.5, // fill opacity for bubbles
strokeWidth = 0.5, // stroke width for bubbles
strokeOpacity, // stroke opacity for bubbles
legendBubbleTitle,
legendColorTitle,
legendX = 600 - maxRadius - 10,
legendY = height - 10
} = {}
) {
// Compute values.
const I = d3.map(data, (_, i) => i);
const V = d3.map(data, sizeValue).map((d) => (d == null ? NaN : +d));
const C = d3.map(data, colorValue).map((d) => (d == null ? NaN : +d));
const P = d3.map(data, position);
const N = d3.map(data, nameValue);
const T = title == null ? null : d3.map(data, title);
const K = d3.map(data, territory);
console.log(V)

// Compute default domains.
if (domain === undefined) domain = [0, 7];

// Construct scales.
const radius = scale(domain, [0, maxRadius]);
// Compute the default height. If an outline object is specified, scale the projection to fit
// the width, and then compute the corresponding height.
if (height === undefined) {
if (outline === undefined) {
height = 400;
} else {
const [[x0, y0], [x1, y1]] = d3
.geoPath(projection.fitWidth(width, outline))
.bounds(outline);
const dy = Math.ceil(y1 - y0),
l = Math.min(Math.ceil(x1 - x0), dy);
projection.scale((projection.scale() * (l - 1)) / l).precision(0.2);
height = dy;
}
}

// Construct a path generator.
const path = d3.geoPath(projection);

// Tooltip
if (createTooltip) {
var div = d3.select("body").append("div")
.attr("class", "arrow_box")
.style("opacity", 0);
}

const svg = d3
.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, 650])
.attr("style", "width: 100%; height: auto; height: intrinsic;");

if (outline != null)
svg
.append("path")
.attr("fill", "white")
.attr("stroke", "currentColor")
.attr("d", path(outline));
// MAPA
svg
.append("path")
.datum(features)
.attr("fill", backgroundFill)
.attr("d", path)
// BORDES MAPA
if (borders != null)
svg
.append("path")
.attr("pointer-events", "none")
.attr("fill", "none")
.attr("stroke", backgroundStroke)
.attr("stroke-linecap", backgroundStrokeLinecap)
.attr("stroke-linejoin", backgroundStrokeLinejoin)
.attr("stroke-width", backgroundStrokeWidth)
.attr("stroke-opacity", backgroundStrokeOpacity)
.attr("d", path(borders))
const legendColor = svg
.append("g")
.attr("transform", `translate(${420}, ${180}) `)
.append(() => categoricalLegend2(legendData, 'Cantidad de desertores por región'))
.attr("fill-opacity", 1);
const legend = svg
.append("g")
.attr("fill", "#777")
.attr("transform", `translate(${legendX},${legendY})`)
.attr("text-anchor", "middle")
.style("font", "10px sans-serif")
.selectAll("g")
.data(radius.ticks(4).slice(1))
.join("g");

legend
.append("circle")
.attr("fill", "none")
.attr("stroke", "#ccc")
.attr("cy", (d) => -radius(d))
.attr("r", radius);

legend
.append("g")
.call(g => g.append("text")
.attr("x", (0))
.attr("y", -45)
.attr("text-anchor", "middle")
.style('font-weight', "700")
.style('font-size', "10px")
.style('font-family', fonts['default']['family'])
.text(legendBubbleTitle));

legend
.append("text")
.attr("y", (d) => -2 * radius(d))
.attr("dy", "0.8em")
.text(radius.tickFormat(6, "s"));
svg.append("g")
.attr("fill-opacity", fillOpacity)
.attr("stroke-width", strokeWidth)
.attr("stroke-opacity", strokeOpacity)
.selectAll("circle")
.data(
d3
.range(data.length)
.filter((i) => P[i])
.sort((i, j) => d3.descending(V[i], V[j]))
)
.join("circle")
.attr("fill", (i) => colorScale(C[i]))
.attr("stroke", (i) => colorScale(C[i]))
.attr("value", (i) => V[i])
.attr("desertores", (i) => C[i])
.attr("region", (i) => N[i])
.attr("territory", (i) => K[i])
.attr(
"transform",
projection == null
? (i) => `translate(${P[i]})`
: (i) => `translate(${projection(P[i])})`
)
.attr("r", (i) => radius(V[i]))

.on('mouseover', function (q, i) {
svg.selectAll("circle")
.attr("fill-opacity", 0.2)
d3.select(this).transition()
.duration('25')
.attr("fill-opacity", 0.8)

//Tooltip
div.html(createTooltip(q, data))
.style("left", (q.pageX + 20) + "px")
.style("top", (q.pageY + 20) + "px")
.transition()
.duration('25')
.style("opacity", 1)
})
.on('mouseout', function (q) {
svg.selectAll("path")
.attr("fill-opacity", 0.8)
d3.select(this).transition()
.duration('25')
//Tooltip
div.transition()
.duration('25')
.style("opacity", 0);
});



// Logo DecideChile
svg.append('image')
.attr('xlink:href', 'https://images.squarespace-cdn.com/content/v1/6078e332d6af970bb7b95e6f/1672340132826-T3GAGDEAALAA6MLWYERN/poweredbyunholster.png?format=500w')
.attr("height", '15')
.attr("transform", `translate(${700}, ${600}) `)
return Object.assign(svg.node(), { scales: { radius } });
}
Insert cell
centroid = {
const path = d3.geoPath();
return feature => path.centroid(feature);
}
Insert cell
createTooltip = function createTooltip(d, data) {

const fallData = data.find(el => el.territoryKey === (d.explicitOriginalTarget.attributes.territory.value));

if (fallData.value === null) {
return `<strong>
${fallData.region}
</strong>
<i class="texto-primario">
S/I
</i>
`
}
else {

return `<strong>
${fallData.region}
</strong>
<span class="tooltip">
<p>Tasa de Incidencia: ${d3.format(',')(Math.round(fallData.value)).replace(/,/g, ".")} </p>
</span>
<span class="tooltip">
<p>Cantidad desertores: ${d3.format(',')(fallData.desertores).replace(/,/g, ".")} </p>
</span>
`}
}


// mouseover - originalTarget - attributes - value desertores
Insert cell
legendData = [
{'name': 's/n', 'color': "#EDEDED"},
{'name': '0', 'color': "#ffffb2" },
{'name': '500', 'color': "#fed976"},
{'name': '1000', 'color': "#feb24c"},
{'name': '1500', 'color': "#fd8d3c"},
{'name': '2000', 'color': "#fc4e2a"},
{'name': '2500+', 'color': "#e31a1c"},
]


//["#ffffb2","#fed976","#feb24c","#fd8d3c","#fc4e2a","#e31a1c","#b10026"]
Insert cell
function categoricalLegend2(data, title){
let n = data.length
let legendData = (i) => data[i]
let svg = d3.create("svg").attr("viewBox", [0, 0, 1000, 100])
svg
.selectAll(".firstrow")
.data(Array.from(Array(n).keys()))
.enter()
.append("circle")
.attr("cx", function(d,i){return 30 + i*40})
.attr("cy", 50)
.attr("r", 10)
.attr("fill", function(d){return legendData(d).color })
svg
.selectAll(".firstrow")
.data(Array.from(Array(n).keys()))
.enter()
.append("g")
.call(g => g.append("text")
.attr("x", function(d,i){return 26 + i*39})
.attr("y", 70)
.style('font-weight', "400")
.style('font-size', "7px")
.style('font-family', fonts['default']['family'])
.text(function(d){return legendData(d).name }));
svg
.append("g")
.call(g => g.append("text")
.attr("x", (-100 + (n/2)*120)/2)
.attr("y", 30)
.attr("text-anchor", "middle")
.style('font-weight', "600")
.style('font-size', "10px")
.style('font-family', fonts['default']['family'])
.text(title));
return svg.node()
}
Insert cell
a = categoricalLegend2(legendData, 'Cantidad de desertores por región')
Insert cell
Insert cell
mapBuildUrl = 'https://s3.amazonaws.com/staging.decidechile.cl/api/v1/maps/m.2020/20200320161219.a8ed46'
Insert cell
baseTopo = fetch(mapBuildUrl + '/t/base.topo.json').then(res => res.json())
Insert cell
topologia = fetch(mapBuildUrl + '/t/base.canonical_key.topo.json').then(r => r.json())
Insert cell
//topologiaGS = fetch('https://www.decidechile.cl/data-vis/mapas/comunas/GranSantiago3.json').then(r => r.json())
topologiaGS = FileAttachment("GranSantiago3.json").json()
Insert cell
comunasPorRegion = filterComunasBy_comuna('Region')
Insert cell
//Object.keys(comunasPorRegion)
regiones = [
"Región de Arica y Parinacota",
"Región de Tarapacá",
"Región de Antofagasta",
"Región de Atacama",
"Región de Coquimbo",
"Región de Valparaíso",
"Región Metropolitana de Santiago",
"Región del Libertador Bernardo O'Higgins",
"Región del Maule",
"Región de Ñuble",
"Región del Bío-Bío",
"Región de La Araucanía",
"Región de Los Ríos",
"Región de Los Lagos",
"Región de Aysén del Gral.Ibañez del Campo",
"Región de Magallanes y Antártica Chilena"
]
Insert cell
topos_comuna = ({
'Regional': filterTopologia_comuna(region, comunasPorRegion)
})
Insert cell
comunasKey = ({
'Regional': comunasPorRegion[region]
})
Insert cell
comunaList = comunasKey[tipoMapa]
Insert cell
getMapConf_comuna = (mapType) => {
const geoObj_comuna = getGeoObj_comuna(mapType);
const mapProjection_comuna = projection_comuna(geoObj_comuna);
return {
geoObj: geoObj_comuna,
mapProjection: mapProjection_comuna
}
}
Insert cell
getGeoObj_comuna = (mapType) => {
const topo = topos_comuna[mapType];
return {
type: 'GeometryCollection',
geometries: topo.geometries.map(comuna => {
const mesh = topo.extra? topojson.mesh(topologiaGS, comuna) : topojson.mesh(topologia, comuna);
return {
...mesh,
properties: {
canonical_key: comuna.properties.canonical_key,
centroid: d3.geoCentroid(mesh)
}
}
})
}
}
Insert cell
filterTopologia_comuna = (filter, distList) => {
const geometries = topologia.objects.base.geometries
if (filter === "Gran Santiago"){
return {
type: 'GeometryCollection',
geometries: topologiaGS.objects.base.geometries,
extra: true
}
}
return {
type: 'GeometryCollection',
geometries: geometries.filter(
geom => distList[filter].includes(geom.properties.canonical_key))
}
}
Insert cell
filterComunasBy_comuna = (filter) => {
const reducer = (key) => {
return function reducer(accumulator, {properties}) {
const {canonical_key} = properties
const dist = properties[key]
if(accumulator[dist]) {
accumulator[dist].push(canonical_key)
} else {
accumulator[dist] = [canonical_key]
}
return accumulator
}
}
let comunas = baseTopo.objects.base.geometries.reduce(reducer(filter), {})
/*
if (filter === 'Region') {
comunas["Gran Santiago"] = topologiaGS.objects.base.geometries.reduce(reducer(filter), {}).undefined
}*/
return comunas
}
Insert cell
projection_comuna = (geom) => {
return d3.geoMercator().fitSize([width, height], geom)
}
Insert cell
viewof region = select({
title: "Región",
selected: "Región Metropolitana de Santiago",
options: regiones,
onchange: objs => {
d3.select(objs.select)
.on('end', () => {
objs.div.update(objs.select.value);
});
}
})
Insert cell
geoObj_comuna = getGeoObj_comuna("Regional")
Insert cell
selectDivDesercion2 = () => {
return html`
<div class="grid-container-select">
<div class="item3">${viewof añoDesercion}</div>
<div class="item5">${viewof nivel}</div>
<div class="item5">${viewof region}</div>
</div> `
}
Insert cell
selector_desercion_comuna = selectDivDesercion2()
Insert cell
viewof bubbleMap_desercion_comunas = BivariateBubbleMap_comuna(desercion_filtrada_comuna2, {
nameValue: ({ COMUNA }) => COMUNA,
sizeValue: ({ TASA_INCIDENCIA_COMUNA }) => +TASA_INCIDENCIA_COMUNA,
colorValue: ({ DESERTORES_COMUNA }) => +DESERTORES_COMUNA,
territory: ({COMUNA_KEY}) => COMUNA_KEY,
position({COMUNA_KEY}) {
const comuna = comunasMap.get(COMUNA_KEY);
return comuna && centroid(comuna);

},
title({COMUNA_KEY}) {
const comuna = comunasMap.get(COMUNA_KEY);
return `${comuna?.properties.COMUNA}\n${(+desercion_filtrada_comuna2).toLocaleString("en")}`;
},
colorScale: colorScale_comuna,
maxRadius:15, // maximum radius of bubbles
width: 140, // outer width, in pixels
height:100, // outer height, in pixels
features: geoObj_comuna,
projection: projection_comuna(geoObj_comuna),
borders: comunaMesh,
width: 975,
height: 610,
//legendColorTitle: "Cantidad de desertores",
legendBubbleTitle: "Tasa de Incidencia",
})
Insert cell
desercion_comuna_llaves2 = FileAttachment("desercion_comuna_llaves2.json").json()
Insert cell
desercion_filtrada_comuna2 = desercion_comuna_llaves2.filter(data => (data.AGNO === añoDesercion) & (data.NIVEL === nivel) & (data.REGION === region) )
Insert cell
comunasMap = new Map(topojson.feature(baseTopo, baseTopo.objects.base).features.map(d => [d.properties.canonical_key, d]))
//
Insert cell
comunaMesh = topojson.mesh(baseTopo, baseTopo.objects.base, (a, b) => a !== b)
Insert cell
BivariateBubbleMap_comuna = function BivariateBubbleMap_comuna(data,
{
position = (e) => e, // given d in data, returns the [longitude, latitude]
sizeValue = () => undefined, // given d in data, returns the quantitative value
nameValue = () => undefined,
colorValue = () => undefined, // given d in data, returns the value to use for the fill and stroke
territory = () => undefined,
title, // given a datum d, returns the hover text
scale = d3.scaleSqrt, // type of radius scale
domain, // [0, max] values; input of radius scale; must start at zero
maxRadius = 15, // maximum radius of bubbles
width = 640, // outer width, in pixels
height , // outer height, in pixels
projection, // a D3 projection; null for pre-projected geometry
features, // a GeoJSON feature collection for the background
borders, // a GeoJSON object for stroking borders
outline = projection && projection.rotate ? { type: "Sphere" } : null, // a GeoJSON object for the background
backgroundFill = "#ededed", // fill color for background
backgroundStroke = "white", // stroke color for borders
backgroundStrokeWidth, // stroke width for borders
backgroundStrokeOpacity, // stroke width for borders
backgroundStrokeLinecap = "round", // stroke line cap for borders
backgroundStrokeLinejoin = "round", // stroke line join for borders
colorScale = d3.scaleLinear(d3.interpolateViridis),
fillOpacity = 0.5, // fill opacity for bubbles
strokeWidth = 0.5, // stroke width for bubbles
strokeOpacity, // stroke opacity for bubbles
legendBubbleTitle,
legendColorTitle,
legendX = 825 - maxRadius - 10,
legendY = height - 10
} = {}
) {
// Compute values.
const I = d3.map(data, (_, i) => i);
const V = d3.map(data, sizeValue).map((d) => (d == null ? NaN : +d));
const C = d3.map(data, colorValue).map((d) => (d == null ? NaN : +d));
const P = d3.map(data, position);
const N = d3.map(data, nameValue);
const T = title == null ? null : d3.map(data, title);
const K = d3.map(data, territory);
console.log(V)

// Compute default domains.
if (domain === undefined) domain = [0, 7];

// Construct scales.
const radius = scale(domain, [0, maxRadius]);
// Compute the default height. If an outline object is specified, scale the projection to fit
// the width, and then compute the corresponding height.
if (height === undefined) {
if (outline === undefined) {
height = 400;
} else {
const [[x0, y0], [x1, y1]] = d3
.geoPath(projection.fitWidth(width, outline))
.bounds(outline);
const dy = Math.ceil(y1 - y0),
l = Math.min(Math.ceil(x1 - x0), dy);
projection.scale((projection.scale() * (l - 1)) / l).precision(0.2);
height = dy;
}
}

// Construct a path generator.
const path = d3.geoPath(projection);

// Tooltip
if (createTooltip_comuna) {
var div = d3.select("body").append("div")
.attr("class", "arrow_box")
.style("opacity", 0);
}

const svg = d3
.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, 650])
.attr("style", "width: 100%; height: auto; height: intrinsic;");

if (outline != null)
svg
.append("path")
.attr("fill", "white")
.attr("stroke", "currentColor")
.attr("d", path(outline));
// MAPA
svg
.append("path")
.datum(features)
.attr("fill", backgroundFill)
.attr("d", path)
// BORDES MAPA
if (borders != null)
svg
.append("path")
.attr("pointer-events", "none")
.attr("fill", "none")
.attr("stroke", backgroundStroke)
.attr("stroke-linecap", backgroundStrokeLinecap)
.attr("stroke-linejoin", backgroundStrokeLinejoin)
.attr("stroke-width", backgroundStrokeWidth)
.attr("stroke-opacity", backgroundStrokeOpacity)
.attr("d", path(borders))
const legendColor = svg
.append("g")
.attr("transform", `translate(${650}, ${100}) `)
.append(() => categoricalLegend_comuna(legendData_comuna, 'Cantidad de desertores por región'))
.attr("fill-opacity", 1);


///////////////
const legend = svg
.append("g")
.attr("fill", "#777")
.attr("transform", `translate(${legendX},${legendY})`)
.attr("text-anchor", "middle")
.style("font", "10px sans-serif")
.selectAll("g")
.data(radius.ticks(4).slice(1))
.join("g");

legend
.append("circle")
.attr("fill", "none")
.attr("stroke", "#ccc")
.attr("cy", (d) => -radius(d))
.attr("r", radius);

legend
.append("g")
.call(g => g.append("text")
.attr("x", (0))
.attr("y", -45)
.attr("text-anchor", "middle")
.style('font-weight', "700")
.style('font-size', "10px")
.style('font-family', fonts['default']['family'])
.text(legendBubbleTitle));

legend
.append("text")
.attr("y", (d) => -2 * radius(d))
.attr("dy", "0.8em")
.text(radius.tickFormat(1, "s"));




//////////////
svg.append("g")
.attr("fill-opacity", fillOpacity)
.attr("stroke-width", strokeWidth)
.attr("stroke-opacity", strokeOpacity)
.selectAll("circle")
.data(
d3
.range(data.length)
.filter((i) => P[i])
.sort((i, j) => d3.descending(V[i], V[j]))
)
.join("circle")
.attr("fill", (i) => colorScale(C[i]))
.attr("stroke", (i) => colorScale(C[i]))
.attr("value", (i) => V[i])
.attr("desertores", (i) => C[i])
.attr("region", (i) => N[i])
.attr("territory", (i) => K[i])
.attr(
"transform",
projection == null
? (i) => `translate(${P[i]})`
: (i) => `translate(${projection(P[i])})`
)
.attr("r", (i) => radius(V[i]))

.on('mouseover', function (q, i) {
svg.selectAll("circle")
.attr("fill-opacity", 0.2)
d3.select(this).transition()
.duration('25')
.attr("fill-opacity", 0.8)

//Tooltip
div.html(createTooltip_comuna(q, data))
.style("left", (q.pageX + 20) + "px")
.style("top", (q.pageY + 20) + "px")
.transition()
.duration('25')
.style("opacity", 1)
})
.on('mouseout', function (q) {
svg.selectAll("path")
.attr("fill-opacity", 0.8)
d3.select(this).transition()
.duration('25')
//Tooltip
div.transition()
.duration('25')
.style("opacity", 0);
});



// Logo DecideChile
svg.append('image')
.attr('xlink:href', 'https://images.squarespace-cdn.com/content/v1/6078e332d6af970bb7b95e6f/1672340132826-T3GAGDEAALAA6MLWYERN/poweredbyunholster.png?format=500w')
.attr("height", '30')
.attr("transform", `translate(${900}, ${600}) `)
return Object.assign(svg.node(), { scales: { radius } });
}
Insert cell
createTooltip_comuna = function createTooltip(d, data) {

const fallData = data.find(el => el.COMUNA_KEY === (d.explicitOriginalTarget.attributes.territory.value));

if (fallData.TASA_INCIDENCIA_COMUNA === null) {
return `<strong>
${fallData.COMUNA}
</strong>
<i class="texto-primario">
S/I
</i>
`
}
else {

return `<strong>
${fallData.COMUNA}
</strong>
<span class="tooltip">
<p>Tasa de Incidencia: ${d3.format(',')(Math.round(fallData.TASA_INCIDENCIA_COMUNA)).replace(/,/g, ".")} </p>
</span>
<span class="tooltip">
<p>Cantidad desertores: ${d3.format(',')(fallData.DESERTORES_COMUNA).replace(/,/g, ".")} </p>
</span>
`}
}


// mouseover - originalTarget - attributes - value desertores
Insert cell
colorScale_comuna = d3
.scaleThreshold()
.domain([0, 20, 40, 60, 80, 100, Math.ceil(extent_comuna[1])])
.range(colores_comuna)
Insert cell
colores_comuna = ["#fed976","#feb24c","#fd8d3c","#fc4e2a","#e31a1c","#b10026"]
Insert cell
extent_comuna = d3.extent(desercion_comuna_llaves2, (d) => +d.DESERTORES_COMUNA)
Insert cell
function categoricalLegend_comuna(data, title){
let n = data.length
let legendData = (i) => data[i]
let svg = d3.create("svg").attr("viewBox", [0, 0, 1000, 100])
svg
.selectAll(".firstrow")
.data(Array.from(Array(n).keys()))
.enter()
.append("circle")
.attr("cx", function(d,i){return 30 + i*40})
.attr("cy", 50)
.attr("r", 10)
.attr("fill", function(d){return legendData(d).color })
svg
.selectAll(".firstrow")
.data(Array.from(Array(n).keys()))
.enter()
.append("g")
.call(g => g.append("text")
.attr("x", function(d,i){return 25 + i*40})
.attr("y", 70)
.style('font-weight', "400")
.style('font-size', "7px")
.style('font-family', fonts['default']['family'])
.text(function(d){return legendData(d).name }));
svg
.append("g")
.call(g => g.append("text")
.attr("x", (-100 + (n/2)*120)/2)
.attr("y", 30)
.attr("text-anchor", "middle")
.style('font-weight', "600")
.style('font-size', "10px")
.style('font-family', fonts['default']['family'])
.text(title));
return svg.node()
}
Insert cell
b = categoricalLegend_comuna(legendData_comuna, 'Cantidad de desertores por región')
Insert cell
legendData_comuna = [
{'name': 's/n', 'color': "#EDEDED"},
{'name': '0', 'color': "#ffffb2" },
{'name': '20', 'color': "#fed976"},
{'name': '40', 'color': "#feb24c"},
{'name': '60', 'color': "#fd8d3c"},
{'name': '80', 'color': "#fc4e2a"},
{'name': '100+', 'color': "#e31a1c"},
]


//["#ffffb2","#fed976","#feb24c","#fd8d3c","#fc4e2a","#e31a1c","#b10026"]
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
themes = `
.input-default button, .input-white button, .input-default select, .input-white select {
outline: none;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
opacity: 0.9;
height: 30px;
margin-left: 0px;
padding: 5px 10px 5px 10px;
background: #EFEFEF;
border: 0.5px solid white;
border-radius: 5px;
color: #262626;
font-family: Avenir, Arial;
font-size: 12px;
font-weight: 600;
cursor: pointer;
}

.input-white button, .input-white select {
color: white;
}

.input-default select, .input-white select {
padding-right: 29px;
}

.input-default .caret, .input-white .caret {
width: 12px;
height: 10px;
margin-left: -24px;
fill: black;
pointer-events: none;
opacity: 0.75;
}

.input-white .caret {
fill: white;
}

.input-default input[type = 'range'], .input-default-thin input[type = 'range'], .input-default-round input[type = 'range'], .input-white input[type = 'range'], .input-white-thin input[type = 'range'], .input-white-round input[type = 'range'] {
appearance: none;
-webkit-appearance: none;
width: 200px;
height: 5px;
border-radius: 1px;
background: #E5E5E5;
}

.input-default input[type = 'range']::-webkit-slider-thumb, .input-white input[type = 'range']::-webkit-slider-thumb {
appearance: none;
-webkit-appearance: none;
width: 15px;
height: 15px;
background: #545454;
cursor: pointer;
border: none;
border-radius: 5px;
}

.input-white input[type = 'range']::-webkit-slider-thumb {
background: #FCFCFC;
border: 1px solid #545454;
}

.input-default-thin input[type = 'range']::-webkit-slider-thumb, .input-white-thin input[type = 'range']::-webkit-slider-thumb {
appearance: none;
-webkit-appearance: none;
width: 7.5px;
height: 20px;
background: #545454;
cursor: pointer;
border: none;
border-radius: 2.5px;
}

.input-white-thin input[type = 'range']::-webkit-slider-thumb {
background: #FCFCFC;
border: 1px solid #545454;
}

.input-default-round input[type = 'range']::-webkit-slider-thumb, .input-white-round input[type = 'range']::-webkit-slider-thumb {
appearance: none;
-webkit-appearance: none;
width: 15px;
height: 15px;
background: #545454;
cursor: pointer;
border: none;
border-radius: 7.5px;
}

.input-white-round input[type = 'range']::-webkit-slider-thumb {
background: #FCFCFC;
border: 1px solid #545454;
}

input[type = 'range']:focus {
outline: none;
}

.output {
margin-left: 5px;
color: #262626;
font-family: Courier;
font-size: 14px;
}

.title {
margin-bottom: 3.5px;
margin-left: 2.5px;
color: black;
font-family: Avenir, Arial;
font-size: 14px;
font-weight: 600;
}

.desc {
margin-top: 6px;
margin-left: 2.5px;
color: #4C4C4C;
font-family: Avenir, Arial;
font-size: 12px;
}
`
Insert cell
viewof tipoMapa = select({
title: "Mapa",
options: [
'Regional',
'País',
]
})
Insert cell
viewof mapa = select({
title: "Mapa",
options: [
'Regional',
'País',
]
})
Insert cell
selectDivDesercion = () => {
if (mapa === "País") {
return html`
<div class="grid-container-select">
<div class="item5">${viewof mapa}</div>
<div class="item3">${viewof añoDesercion}</div>
<div class="item5">${viewof nivel}</div>
</div> `}

return html`
<div class="grid-container-select">
<div class="item5">${viewof mapa}</div>
<div class="item3">${viewof añoDesercion}</div>
<div class="item5">${viewof nivel}</div>
<div class="item5">${viewof region}</div>

</div> `
}
Insert cell
showMap = () => {
if (mapa === 'País') {
return html`
${viewof bubbleMap_desercion_regiones}`
}
return html`${viewof bubbleMap_desercion_comunas}`}
Insert cell
selector_desercion = selectDivDesercion()
Insert cell
mapa_pais = showMap()
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more