Published
Edited
Jul 8, 2021
Importers
1 star
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
classInteractive.renderGraphic()
Insert cell
breaks_colors = ["#fffedb","#ffe7a1", "#ffcd60", "#ffa934", "#e27510", "#ab4603"]
Insert cell
breaks_col = geoda.naturalBreaks(6, column_selected)
Insert cell
classInteractive = new LinkedInteractiveMap({
data: data,
geojson: geojson,
breaks: breaks_col, // breaks for lisa values
colorBins: breaks_colors,
colorSig: lm.colors,
column: column,
ordinalLabels: breaks_col,
colorVariable: 'Donatns',
sigVar: 'cluster',
id_var: id_var,
chartTitle: 'Hello'
})
Insert cell
class LinkedInteractiveMap {
constructor({
geojson={},
data={},
colorBins=[],
breaks=[],
ordinalLabels=[],
id_var='',
colorVariable='',
colorSig = '',
column = '',
sigVar = '',
chartTitle = '',
}){
this.geojson= geojson;
this.data=data;
this.colorBins=colorBins;
this.breaks=breaks;
this.ordinalLabels=ordinalLabels;
this.id_var=id_var;
this.colorVariable=colorVariable;
this.column = column;
this.rendered = false;
this.transitionTime = 25;
this.colorScale = d3.scaleThreshold()
.domain(breaks)
.range(colorBins);
this.colorScaleSig = d3.scaleThreshold()
.domain([1,2,3,4,5,6])
.range(colorSig);
this.encoding = {
x: 'stdVal',
y: 'spatialLag',
c: colorVariable,
sig: sigVar,
};
this.encodingDefaults = {
x: 'stdVal',
y: 'spatialLag',
c: colorVariable,
sig: sigVar,
};
this.lock_active_zip=false;
this.active_zip=null;
this.neighborTarget=[]; // Default data types as expecting for methods like .includes() !
this.mouseoutTimeout=null;
this.title = chartTitle;
}


update(){
const infoBox = document.querySelector('#infoboxContainer');

d3.selectAll('.label')
.attr("visibility", "hidden")
if (this.active_zip !== undefined){
d3.selectAll('.cell')
.select('circle')
.attr('stroke', d => d.data[this.id_var] == this.active_zip[this.id_var]
? "black"
: this.active_zip.locNeighbors.includes(d.data[this.id_var])
? '#999999'
: "currentColor")
.attr('stroke-width', d => d.data[this.id_var] == this.active_zip[this.id_var]
? 2
: this.active_zip.locNeighbors.includes(d.data[this.id_var])
? 2
: 1)
.style('opacity', d => d.data[this.id_var] == this.active_zip[this.id_var]
? 1
: this.active_zip.locNeighbors.includes(d.data[this.id_var])
? 1
: 0.5
);

d3.selectAll(`#label-${this.active_zip[this.id_var]}`)
// .filter(d => {console.log('label', d); return d.data[this.id_var] == this.active_zip[this.id_var]})
.attr("visibility", "visible")
d3.selectAll('.polygon')
.attr('stroke-width', d => d.properties[this.id_var] == this.active_zip[this.id_var]
? "5" : "1"
)
.style('opacity', d => d.properties[this.id_var] == this.active_zip[this.id_var]
? 1
: this.active_zip.locNeighbors.includes(d.properties[this.id_var])
? 0.8
: 0.2
)

d3.selectAll('.tract')
.select('path')
.attr('stroke', d => d.properties[this.id_var] == this.active_zip[this.id_var]
? this.colorScaleSig(d.properties['cluster']) : "#999999"
)
.attr('paint-order', 'stroke');

d3.selectAll('.tract')
.filter(d => d.properties[this.id_var] == this.active_zip[this.id_var])
.raise()

d3.selectAll('.tract')
.filter(d => this.active_zip.locNeighbors.includes(d.properties[this.id_var]))
.raise()


infoBox.innerHTML = `<div class="infobox">
<h4>${this.active_zip[id_var]}</h4>
<h5>${this.active_zip.Dprtmnt}</h5>
<table>
<tr>
<th>Cluster</th>
<td>${lm.labels[this.active_zip.cluster]}</td>
</tr>
<tr>
<th>${column}</th>
<td>${this.active_zip[column]}</td>
</tr>
<tr>
<th>Number of Neighbors</th>
<td>${this.active_zip.numNeighbors}</td>
</tr>
<tr>
<th>Lisa Value</th>
<td>${this.active_zip.lisaValue}</td>
</tr>
<tr>
<th>P-Value</th>
<td>${this.active_zip.pVal}</td>
</tr>
</table>
</div>`;
} else {
infoBox.innerHTML = '';
d3.selectAll('.polygon')
.attr('stroke-width', 1)
.style('opacity', 1);

d3.selectAll('.cell')
.select('circle')
.attr('stroke', 'currentColor')
.attr('stroke-width', 1)
.style('opacity', 1);

d3.selectAll('.tract')
.select('path')
.attr('stroke', '#999999');

d3.selectAll('.label')
.attr("visibility", "hidden")

}

}
renderGraphic(){
const svg = d3.select(DOM.svg(width, 600))
.attr('color', '#999')
.attr('font-family', "'Helvetica Neue', sans-serif")
.attr('font-size', 12);
const chart = svg.append('g')
.attr('transform', `translate(${chart_offset.x}, ${chart_offset.y+30})`)
.attr('class', 'chart');
const infoBox = html`<div id="infoboxContainer"></div>`;
const styleLabel = label => label
.attr('text-anchor', 'middle')
.attr('dominant-baseline', 'bottom')
.attr('stroke-width', '2')
.attr('fill', '#fff')
.attr('paint-order', 'stroke')
.attr('font-weight', 'bold')
.attr('font-size', 14)
.attr('pointer-events', 'none')
.attr('visibility', 'hidden')
.style('text-shadow', '0 0 3px #000, 0 0 3px #000');
const styleCircle = circle => circle
.attr('stroke-width', 1)
.attr('stroke', 'currentColor')
.attr('stroke-opacity', 1)
.attr('fill', 'currentColor')
.attr('fill-opacity', 0.8);
const stylePolygon = polygon => polygon
.attr('stroke', '#999999')
.attr('stroke-width', '0.05vw')
.attr('stroke-linejoin', 'round')
.attr('paint-order', 'normal');
const axes = chart.append('g').attr('class', 'axes');
let lastXFormat = ',d';
let lastYFormat = ',d';
let xAxis = axes.append('g').call(x_axis(lastXFormat))
let yAxis = axes.append('g').call(y_axis(lastYFormat))
const xLabel = chart.append('text')
.attr('class', 'direction-label direction-label-x')
.attr('color', '#333')
.attr('fill', 'currentColor')
.attr('text-anchor', 'middle')
.attr('x', chart_width / 2)
.attr('y', chart_height + 20);
const yLabel = chart.append('text')
.attr('class', 'direction-label direction-label-y')
.attr('fill', '#333')
.attr('text-anchor', 'middle')
.attr('x', -chart_height / 2)
.attr('y', 6)
.attr('dy', is_narrow ? '-1em' : '-2em')
.attr('transform', 'rotate(-90)');
const title = chart.append('text')
.attr('class', 'title')
.attr('color', '#000')
.attr('fill', 'currentColor')
.attr('text-anchor', 'middle')
.attr('font-size', 20)
.attr('font-weight', "bold")
.attr('font-family', 'serif')
.attr('x', chart_width / 2)
.attr('y', chart_height-320)
.text(this.title);
const chartContent = chart
.append('g')
.attr('class', 'chart-content')
const cells = chartContent
.selectAll('g')
.data(voronoi(this.data).polygons(), d => d && d.data && d.data[this.id_var])
.join('g')
.attr('class', 'cell')
.attr('data-id', d => d.data[this.id_var])
.style('transition', '50ms all')
.attr('cursor', 'pointer');
const circles = cells
.append('circle')
.call(styleCircle);
const labels = cells
.append('text')
.attr('class', 'label')
.attr('dy', '-0.5em')
.text(d => d && d.data[this.id_var])
.attr('id', d => `label-${d.data[this.id_var]}`)
.call(styleLabel);
const line = chartContent.append('line')
.attr('fill', 'none')
.attr('stroke', 'red')
.attr('stroke-width', 2)
.attr('pointer-events', 'none')
.datum(linear_regression(data));
const yBase = chartContent.append('line')
.attr('fill', 'none')
.attr('stroke', 'black')
.attr('stroke-width', 2)
.attr('pointer-events', 'none');
const xBase = chartContent.append('line')
.attr('fill', 'none')
.attr('stroke', 'black')
.attr('stroke-width', 2)
.attr('pointer-events', 'none');
let mapX = map_margin.left;
let mapY = map_margin.top;
if (is_narrow) {
mapY += chart_offset.y + chart_height;
} else {
const mapHeight = height - map_margin.top - map_margin.bottom;
const mapWidth = mapHeight * (2 - map_aspect_ratio);
mapX = width - mapWidth - map_margin.right;
}
const map = svg.append('g')
.attr('transform', `translate(${mapX-150}, ${mapY-10})`)
.attr('class', 'map');

const options = Object.assign({
color: this.ordinalLabels.length
? d3.scaleOrdinal(this.ordinalLabels, this.colorBins)
: this.colorScale,
});
const legendSVG = legend(options);
const legendWidth = +d3.select(legendSVG).attr('width');
const legendHeight = +d3.select(legendSVG).attr('height');
const legendNode = svg.append('g')
.attr('transform', `translate(${mapX-80}, ${mapY+400})`);
Array.from(legendSVG.children).forEach((child) => {
legendNode.node().appendChild(child);
});
legendNode.select('g').selectAll('text')
.filter(function() { return d3.select(this).attr('font-weight') === 'bold'; })
.attr('font-size', 14);

// const legendNode = svg.append('g')
// if (this.ordinalLabels.length>6) {
// this.ordinalLabels.forEach((d, i) => {
// if (i%2==0){
// legendNode.append('rect')
// .attr('x',i*60+mapX-50)
// .attr('y',mapY+470)
// .attr('width', 20)
// .attr('height',20)
// .attr('fill', this.colorBins[i]);
// legendNode.append('text')
// .attr('x',i*60+mapX-25)
// .attr('y',mapY+470)
// .attr("dy", "1.1em")
// .text(() => this.ordinalLabels[i]);
// }
// else{
// legendNode.append('rect')
// .attr('x',(i-1)*60+mapX-50)
// .attr('y',mapY+490)
// .attr('width', 20)
// .attr('height',20)
// .attr('fill', this.colorBins[i]);
// legendNode.append('text')
// .attr('x',(i-1)*60+mapX-25)
// .attr('y',mapY+490)
// .attr("dy", "1.1em")
// .text(() => this.ordinalLabels[i]);
// }
// })
// }
// if (this.ordinalLabels.length<=6) {
// this.ordinalLabels.forEach((d, i) => {
// if (i%2==0){
// legendNode.append('rect')
// .attr('x',i*60+mapX-50)
// .attr('y',mapY+470)
// .attr('width', 20)
// .attr('height',20)
// .attr('fill', this.colorBins[i]);
// legendNode.append('text')
// .attr('x',i*60+mapX-25)
// .attr('y',mapY+470)
// .attr("dy", "1.1em")
// .text(() => this.ordinalLabels[i]);
// }
// else{
// legendNode.append('rect')
// .attr('x',(i-1)*60+mapX-50)
// .attr('y',mapY+490)
// .attr('width', 20)
// .attr('height',20)
// .attr('fill', this.colorBins[i]);
// legendNode.append('text')
// .attr('x',(i-1)*60+mapX-25)
// .attr('y',mapY+490)
// .attr("dy", "1.1em")
// .text(() => this.ordinalLabels[i]);
// }
// })
// }
const tracts = map
.selectAll('g')
.data(geojson.features)
.join('g')
.attr('class', 'tract')
.attr('cursor', 'pointer');
const geoPath = d3.geoPath(projection);
const polygons = tracts
.append('path')
.attr('class', 'polygon')
.attr('fill', d => this.colorScale(d.properties[this.encodingDefaults.c]))
.attr('data-id', d => d.properties[this.id_var])
.attr('d', geoPath)
.call(stylePolygon);


tracts
.append('text')
.attr('class', 'label')
.text(d => d.properties[this.id_var])
.attr('x', d => geoPath.centroid(d)[0])
.attr('id', d => `label-${d.properties[this.id_var]}`)
.attr('y', d => geoPath.bounds(d)[0][1] - 3)
.call(styleLabel);
[[cells, d => d.data], [tracts, d => d.properties]]
.forEach(([els, accessor]) => {
els
.on('mouseover', (data) => {
if (this.lock_active_zip) return;
const zip = accessor(data);
// if (zip.population) {
if (zip[column]) {
clearTimeout(this.mouseoutTimeout);
this.active_zip = zip;
this.update()
}
})
.on('mouseout', () => {
if (this.lock_active_zip) return;
this.mouseoutTimeout = setTimeout(() => {
this.active_zip = undefined;
this.update()
}, 100);
})
.on('click', (data) => {
const zip = accessor(data);
console.log(zip)
if (this.lock_active_zip && this.active_zip === zip) {
this.lock_active_zip = false;
this.active_zip = undefined;
this.neighborTarget = undefined;
this.update()
} else if (zip[column]) {
clearTimeout(this.mouseoutTimeout);
this.lock_active_zip = true;
this.active_zip = zip;
this.neighborTarget = zip.locNeighbors;
this.update()
}
});
});
const t = svg.transition().duration(this.rendered ? 300 : 0);
x.domain(d3.extent(data, d => d[this.encoding.x]));
//[d3.extent(data, d => d[this.encoding.x])[0]-1, d3.extent(data, d => d[this.encoding.x])[1]]
y.domain(d3.extent(data, d => d[this.encoding.y]));
//this.colorScale.domain(d3.extent(data, d => d[this.encoding.c]));
//size.domain(d3.extent(data, d => d[this.encoding.s]));
voronoi.x(d => x(d[this.encoding.x])).y(d => y(d[this.encoding.y]));
linear_regression.x(d => d[this.encoding.x]).y(d => d[this.encoding.y]);
const regression = linear_regression(data);
xLabel.text(`${column} (Standardized) ⟶`);
yLabel.text(`Spatially Lagged ${column} ⟶`);
cells
.data(voronoi(data).polygons(), d => d && d.data && d.data[id_var])
.join('g');
cells.selectAll('path').remove();
cells
.append('path')
.attr('d', d => d && d.join && `M${d.join('L')}Z`)
.attr('fill', 'none')
// .attr('stroke', 'red')
.attr('pointer-events', 'all');
labels
.attr('x', d => d && d.data && x(d.data[this.encoding.x]))
.attr('y', d => d && d.data && y(d.data[this.encoding.y]));
circles
.attr('cx', d => d && d.data && x(d.data[this.encoding.x]))
.attr('cy', d => d && d.data && y(d.data[this.encoding.y]))
.attr('r', 5)
.attr('color', d => d && d.data && this.colorScaleSig(d.data[this.encoding.sig]))
line
.datum(regression)
.attr('x1', d => x(d[0][0]))
.attr('x2', d => x(d[1][0]))
.attr('y1', d => y(d[0][1]))
.attr('y2', d => y(d[1][1]));

yBase
.attr('x1', d => x(0))
.attr('x2', d => x(0))
.attr('y1', d => y(d3.extent(this.data, d => d[this.encoding.y])[0]))
.attr('y2', d => y(d3.extent(this.data, d => d[this.encoding.y])[1]));
xBase
.attr('x1', d => x(d3.extent(this.data, d => d[this.encoding.x])[0]))
.attr('x2', d => x(d3.extent(this.data, d => d[this.encoding.x])[1]))
.attr('y1', d => y(0))
.attr('y2', d => y(0));
polygons
.attr('fill', d => {
//console.log(this.colorScale(d.properties[this.encoding.c]))
return this.colorScale(d.properties[this.encoding.c])
//d.properties[this.encoding.c])
})
.attr("stroke", d => neighborTarget && this.neighborTarget.includes(d.properties[this.id_var]) ? '#9233AB' :
"#999999")
.attr("stroke-width", d => neighborTarget && this.neighborTarget.includes(d.properties[this.id_var]) ? 2 : 1)
.style('opacity', d => neighborTarget === null || this.neighborTarget.includes(d.properties[this.id_var]) ? 1 : .4);
this.rendered = true;
return Object.assign(html`<div style="position: relative;">${[svg.node(), infoBox]}</div>`);
}
}
Insert cell
mapsInteractive.renderGraphic()
Insert cell
mapsInteractive = new LinkedMap({
data: data,
geojson: geojson,
geojson1: geojson,
breaks: breaks_col,
breaks1: [1,2,3,4,5,6,7],
colorBins: breaks_colors,
colorBins1: lm.colors,
colorSig: lm.colors,
column: column,
ordinalLabels: breaks_col,
ordinalLabels1: lm.labels,
colorVariable: 'Donatns',
colorVariable1: 'cluster',
sigVar: 'cluster',
id_var: id_var,
chartTitle: 'Hello'
})
Insert cell
class LinkedMap {
constructor({
geojson={},
geojson1={},
data={},
colorBins=[],
colorBins1=[],
breaks=[],
breaks1=[],
ordinalLabels=[],
ordinalLabels1=[],
id_var='',
colorVariable='',
colorVariable1='',
colorSig = '',
column = '',
sigVar = '',
chartTitle = '',
}){
this.geojson= geojson;
this.geojson1 = geojson1;
this.data=data;
this.colorBins=colorBins;
this.colorBins1=colorBins1;
this.breaks=breaks;
this.breaks1=breaks1;
this.ordinalLabels=ordinalLabels;
this.ordinalLabels1=ordinalLabels1;
this.id_var=id_var;
this.colorVariable=colorVariable;
this.colorVariable1=colorVariable1;
this.column = column;
this.rendered = false;
this.transitionTime = 25;
this.colorScale = d3.scaleThreshold()
.domain(breaks)
.range(colorBins);
this.colorScale1 = d3.scaleThreshold()
.domain(breaks1)
.range(colorBins1);
this.colorScaleSig = d3.scaleThreshold()
.domain([1,2,3,4,5,6])
.range(colorSig);
this.encoding = {
x: 'stdVal',
y: 'spatialLag',
c: colorVariable,
sig: sigVar,
};
this.encodingDefaults = {
x: 'stdVal',
y: 'spatialLag',
c: colorVariable,
sig: sigVar,
};
this.lock_active_zip=false;
this.active_zip=null;
this.neighborTarget=[]; // Default data types as expecting for methods like .includes() !
this.mouseoutTimeout=null;
this.title = chartTitle;
}


update(){

d3.selectAll('.label')
.attr("visibility", "hidden")
if (this.active_zip !== undefined){

d3.selectAll(`#label-${this.active_zip[this.id_var]}`)
// .filter(d => {console.log('label', d); return d.data[this.id_var] == this.active_zip[this.id_var]})
.attr("visibility", "visible")
d3.selectAll('.polygon')
.attr('stroke-width', d => d.properties[this.id_var] == this.active_zip[this.id_var]
? "5" : "1"
)
.style('opacity', d => d.properties[this.id_var] == this.active_zip[this.id_var]
? 1
: this.active_zip.locNeighbors.includes(d.properties[this.id_var])
? 0.8
: 0.2
)

d3.selectAll('.tract')
.select('path')
.attr('stroke', d => d.properties[this.id_var] == this.active_zip[this.id_var]
? "black" : 'black'
)
.attr('paint-order', 'stroke');

d3.selectAll('.tract')
.filter(d => d.properties[this.id_var] == this.active_zip[this.id_var])
.raise()

d3.selectAll('.tract')
.filter(d => this.active_zip.locNeighbors.includes(d.properties[this.id_var]))
.raise()
} else {

d3.selectAll('.polygon')
.attr('stroke-width', 1)
.style('opacity', 1);

d3.selectAll('.cell')
.select('circle')
.attr('stroke', 'currentColor')
.attr('stroke-width', 1)
.style('opacity', 1);

d3.selectAll('.tract')
.select('path')
.attr('stroke', 'black');

d3.selectAll('.label')
.attr("visibility", "hidden")

}

}
renderGraphic(){
const svg = d3.select(DOM.svg(width, 600))
.attr('color', '#999')
.attr('font-family', "'Helvetica Neue', sans-serif")
.attr('font-size', 12);
const styleLabel = label => label
.attr('text-anchor', 'middle')
.attr('dominant-baseline', 'bottom')
.attr('stroke-width', '2')
.attr('fill', '#fff')
.attr('paint-order', 'stroke')
.attr('font-weight', 'bold')
.attr('font-size', 14)
.attr('pointer-events', 'none')
.attr('visibility', 'hidden')
.style('text-shadow', '0 0 3px #000, 0 0 3px #000');
const styleCircle = circle => circle
.attr('stroke-width', 1)
.attr('stroke', 'currentColor')
.attr('stroke-opacity', 1)
.attr('fill', 'currentColor')
.attr('fill-opacity', 0.8);
const stylePolygon = polygon => polygon
.attr('stroke', '#999999')
.attr('stroke-width', '0.05vw')
.attr('stroke-linejoin', 'round')
.attr('paint-order', 'normal');
let mapX = map_margin.left;
let mapY = map_margin.top;
if (is_narrow) {
mapY += chart_offset.y + chart_height;
} else {
const mapHeight = height - map_margin.top - map_margin.bottom;
const mapWidth = mapHeight * (2 - map_aspect_ratio);
mapX = width - mapWidth - map_margin.right;
}



// MAP left
const map = svg.append('g')
.attr('transform', `translate(${50}, ${mapY-10})`)
.attr('class', 'map');
const tracts = map
.selectAll('g')
.data(geojson.features)
.join('g')
.attr('class', 'tract')
.attr('cursor', 'pointer');
const geoPath = d3.geoPath(projection);
const polygons = tracts
.append('path')
.attr('class', 'polygon')
.attr('fill', d => this.colorScale(d.properties[this.colorVariable]))
.attr('data-id', d => d.properties[this.id_var])
.attr('d', geoPath)
.call(stylePolygon);
const title = map.append('text')
.attr('class', 'title')
.attr('color', '#000')
.attr('fill', 'currentColor')
.attr('text-anchor', 'middle')
.attr('font-size', 20)
.attr('font-weight', "bold")
.attr('font-family', 'serif')
.attr('x', 100)
.attr('y', 20)
.text("Natural Breaks Map");


//MAP right
const map1 = svg.append('g')
.attr('transform', `translate(${mapX-150}, ${mapY-10})`)
.attr('class', 'map');
const tracts1 = map1
.selectAll('g')
.data(this.geojson1.features)
.join('g')
.attr('class', 'tract')
.attr('cursor', 'pointer');
const geoPath1 = d3.geoPath(projection);
const polygons1 = tracts1
.append('path')
.attr('class', 'polygon')
.attr('fill', d => this.colorScale1(d.properties[this.colorVariable1]))
.attr('data-id', d => d.properties[this.id_var])
.attr('d', geoPath1)
.call(stylePolygon);
const title1 = map1.append('text')
.attr('class', 'title')
.attr('color', '#000')
.attr('fill', 'currentColor')
.attr('text-anchor', 'middle')
.attr('font-size', 20)
.attr('font-weight', "bold")
.attr('font-family', 'serif')
.attr('x', 100)
.attr('y', 20)
.text("Cluster Map");


// LEGEND left
const options = Object.assign({
color: this.ordinalLabels.length
? d3.scaleOrdinal(this.ordinalLabels, this.colorBins)
: this.colorScale,
});
const legendSVG = legend(options);
const legendWidth = +d3.select(legendSVG).attr('width');
const legendHeight = +d3.select(legendSVG).attr('height');
const legendNode = svg.append('g')
.attr('transform', `translate(${110}, ${mapY+400})`);
Array.from(legendSVG.children).forEach((child) => {
legendNode.node().appendChild(child);
});
legendNode.select('g').selectAll('text')
.filter(function() { return d3.select(this).attr('font-weight') === 'bold'; })
.attr('font-size', 14);

//LEGEND right
const legendNode1 = svg.append('g')
this.ordinalLabels1.forEach((d, i) => {
if (i%2==0){
legendNode1.append('rect')
.attr('x',i*60+mapX-150)
.attr('y',mapY+420)
.attr('width', 20)
.attr('height',20)
.attr('fill', this.colorBins1[i]);
legendNode1.append('text')
.attr('x',i*60+mapX-125)
.attr('y',mapY+420)
.attr("dy", "1.1em")
.text(() => this.ordinalLabels1[i]);
}
else{
legendNode1.append('rect')
.attr('x',(i-1)*60+mapX-150)
.attr('y',mapY+440)
.attr('width', 20)
.attr('height',20)
.attr('fill', this.colorBins1[i]);
legendNode1.append('text')
.attr('x',(i-1)*60+mapX-125)
.attr('y',mapY+440)
.attr("dy", "1.1em")
.text(() => this.ordinalLabels1[i]);
}
})
tracts
.append('text')
.attr('class', 'label')
.text(d => d.properties[this.id_var])
.attr('x', d => geoPath.centroid(d)[0])
.attr('id', d => `label-${d.properties[this.id_var]}`)
.attr('y', d => geoPath.bounds(d)[0][1] - 3)
.call(styleLabel);

tracts1
.append('text')
.attr('class', 'label')
.text(d => d.properties[this.id_var])
.attr('x', d => geoPath1.centroid(d)[0])
.attr('id', d => `label-${d.properties[this.id_var]}`)
.attr('y', d => geoPath1.bounds(d)[0][1] - 3)
.call(styleLabel);
[[tracts1, d => d.properties], [tracts, d => d.properties]]
.forEach(([els, accessor]) => {
els
.on('mouseover', (data) => {
if (this.lock_active_zip) return;
const zip = accessor(data);
// if (zip.population) {
if (zip[column]) {
clearTimeout(this.mouseoutTimeout);
this.active_zip = zip;
this.update()
}
})
.on('mouseout', () => {
if (this.lock_active_zip) return;
this.mouseoutTimeout = setTimeout(() => {
this.active_zip = undefined;
this.update()
}, 100);
})
.on('click', (data) => {
const zip = accessor(data);
console.log(zip)
if (this.lock_active_zip && this.active_zip === zip) {
this.lock_active_zip = false;
this.active_zip = undefined;
this.neighborTarget = undefined;
this.update()
} else if (zip[column]) {
clearTimeout(this.mouseoutTimeout);
this.lock_active_zip = true;
this.active_zip = zip;
this.neighborTarget = zip.locNeighbors;
this.update()
}
});
});

polygons
.attr('fill', d => {
//console.log(this.colorScale(d.properties[this.encoding.c]))
return this.colorScale(d.properties[this.encoding.c])
//d.properties[this.encoding.c])
})
this.rendered = true;
return Object.assign(html`<div style="position: relative;">${[svg.node()]}</div>`);
}
}
Insert cell
Insert cell
Insert cell
Insert cell
data = {
for (let i=0; i<data_inc.length; i++)
{
data_inc[i].stdVal = std_vals[i]
data_inc[i].spatialLag = spatial_lags[i]
data_inc[i].lisaValue = lm.lisaValues[i]
data_inc[i].cluster = lm.clusters[i]
data_inc[i].pVal = lm.pvalues[i]
data_inc[i].numNeighbors = lm.neighbors[i]
data_inc[i].locNeighbors = neighbors[data_inc[i][id_var]]
}
return data_inc;
}
Insert cell
contInteractive.renderGraphic()
Insert cell
contInteractive = new LinkedContiguityMap({
data: data,
geojson: geojson,
geojson1: geojson,
conn: conn,
weightType: 'Rook',
breaks: breaks_col, // breaks for lisa values
colorBins: breaks_colors,
colorSig: lm.colors,
column: column,
ordinalLabels: breaks_col,
colorVariable: 'Donatns',
sigVar: 'cluster',
id_var: id_var,
chartTitle: 'Hello'
})
Insert cell
class LinkedContiguityMap {
constructor({
geojson={},
geojson1={},
conn={},
data={},
colorBins=[],
weightType='',
breaks=[],
ordinalLabels=[],
id_var='',
colorVariable='',
colorSig = '',
column = '',
sigVar = '',
chartTitle = '',
}){
this.geojson= geojson;
this.geojson1 = geojson1;
this.data=data;
this.conn = conn;
this.weightType = weightType;
this.colorBins=colorBins;
this.breaks=breaks;
this.ordinalLabels=ordinalLabels;
this.id_var=id_var;
this.colorVariable=colorVariable;
this.column = column;
this.rendered = false;
this.transitionTime = 25;
this.colorScale = d3.scaleThreshold()
.domain(breaks)
.range(colorBins);
this.colorScaleSig = d3.scaleThreshold()
.domain([1,2,3,4,5,6])
.range(colorSig);
this.encoding = {
x: 'stdVal',
y: 'spatialLag',
c: colorVariable,
sig: sigVar,
};
this.encodingDefaults = {
x: 'stdVal',
y: 'spatialLag',
c: colorVariable,
sig: sigVar,
};
this.lock_active_zip=false;
this.active_zip=null;
this.neighborTarget=[]; // Default data types as expecting for methods like .includes() !
this.mouseoutTimeout=null;
this.title = chartTitle;
}


update(){

d3.selectAll('.label')
.attr("visibility", "hidden")
if (this.active_zip !== undefined){

d3.selectAll(`#label-${this.active_zip[this.id_var]}`)
// .filter(d => {console.log('label', d); return d.data[this.id_var] == this.active_zip[this.id_var]})
.attr("visibility", "visible")
d3.selectAll('.polygon')
.attr('stroke-width', d => d.properties[this.id_var] == this.active_zip[this.id_var]
? "5" : "1"
)
.style('opacity', d => d.properties[this.id_var] == this.active_zip[this.id_var]
? 1
: this.active_zip.locNeighbors.includes(d.properties[this.id_var])
? 0.8
: 0.2
)

d3.selectAll('.tract')
.select('path')
.attr('stroke', d => d.properties[this.id_var] == this.active_zip[this.id_var]
? "black" : 'black'
)
.attr('paint-order', 'stroke');

d3.selectAll('.tract')
.filter(d => d.properties[this.id_var] == this.active_zip[this.id_var])
.raise()

d3.selectAll('.tract')
.filter(d => this.active_zip.locNeighbors.includes(d.properties[this.id_var]))
.raise()

//contiguity map


d3.selectAll('.tract1')
.filter(d => d.properties[this.id_var] == this.active_zip[this.id_var])
.raise()

d3.selectAll('.polygon1')
// .attr('stroke-width', d => d.properties[this.id_var] == this.active_zip[this.id_var]
// ? "5" : "1"
// )
.attr('fill', d => d.properties[this.id_var] == this.active_zip[this.id_var]
? "yellow"
: this.active_zip.locNeighbors.includes(d.properties[this.id_var])
? "#999999"
: "white"
)
.style('opacity', d => d.properties[this.id_var] == this.active_zip[this.id_var]
? 0.5
: this.active_zip.locNeighbors.includes(d.properties[this.id_var])
? 0.5
: 1
)

d3.selectAll('.tract1')
.filter(d => d.properties[this.id_var] == this.active_zip[this.id_var])
.lower()

} else {

d3.selectAll('.tract').lower()
d3.selectAll('.polygon').lower()
d3.selectAll('.polygon')
.attr('stroke-width', 1)
.style('opacity', 1)

d3.selectAll('.polygon1')
.attr('fill', "white")
.style('opacity', 1)

d3.selectAll('.tract')
.select('path')
.attr('stroke', 'black');

// contiguity

d3.selectAll('.tract1').lower()
d3.selectAll('.label')
.attr("visibility", "hidden")

}

}
renderGraphic(){
const svg = d3.select(DOM.svg(width, 600))
.attr('color', '#999')
.attr('font-family', "'Helvetica Neue', sans-serif")
.attr('font-size', 12);
const styleLabel = label => label
.attr('text-anchor', 'middle')
.attr('dominant-baseline', 'bottom')
.attr('stroke-width', '2')
.attr('fill', '#fff')
.attr('paint-order', 'stroke')
.attr('font-weight', 'bold')
.attr('font-size', 14)
.attr('pointer-events', 'none')
.attr('visibility', 'hidden')
.style('text-shadow', '0 0 3px #000, 0 0 3px #000');
const styleCircle = circle => circle
.attr('stroke-width', 1)
.attr('stroke', 'currentColor')
.attr('stroke-opacity', 1)
.attr('fill', 'currentColor')
.attr('fill-opacity', 0.8);
const stylePolygon = polygon => polygon
.attr('stroke', 'black')
.attr('stroke-width', '0.05vw')
.attr('stroke-linejoin', 'round')
.attr('paint-order', 'normal');
let mapX = map_margin.left;
let mapY = map_margin.top;
if (is_narrow) {
mapY += chart_offset.y + chart_height;
} else {
const mapHeight = height - map_margin.top - map_margin.bottom;
const mapWidth = mapHeight * (2 - map_aspect_ratio);
mapX = width - mapWidth - map_margin.right;
}





//MAP 1 (contiguity)
const map1 = svg.append('g')
.attr('transform', `translate(${50}, ${mapY-10})`)
.attr('class', 'map');
const tracts1 = map1
.selectAll('g')
.data(this.geojson1.features)
.join('g')
.attr('class', 'tract1')
.attr('cursor', 'pointer');
const geoPath1 = d3.geoPath(projection);
const polygons1 = tracts1
.append('path')
.attr('class', 'polygon1')
.attr('fill', 'white')
.attr('data-id', d => d.properties[this.id_var])
.attr('d', geoPath1)
.call(stylePolygon);
const circles = map1
.append('g')
.attr('class', 'circle');
circles
.selectAll('circle')
.data(this.conn.targets)
.enter()
.append("circle")
.attr('cx', d => {
return projection(d.position)[0];
})
.attr('cy', d => {
return projection(d.position)[1];
})
.attr('r', 3)
.attr('color','orange');
const arcs = map1
.append("g")
.attr("class","arcs");
arcs.selectAll("path")
.data(this.conn.arcs)
.enter()
.append("path")
.attr('d', d => {console.log(d);
return lngLatToArc(d, d.source, d.target);
})
const title = map1.append('text')
.attr('class', 'title')
.attr('color', '#000')
.attr('fill', 'currentColor')
.attr('text-anchor', 'middle')
.attr('font-size', 20)
.attr('font-weight', "bold")
.attr('font-family', 'serif')
.attr('x', 230)
.attr('y', mapY+400)
.text(this.weightType+ " Contiguity");
// MAP original
const map = svg.append('g')
.attr('transform', `translate(${mapX-150}, ${mapY-10})`)
.attr('class', 'map');
const tracts = map
.selectAll('g')
.data(geojson.features)
.join('g')
.attr('class', 'tract')
.attr('cursor', 'pointer');
const geoPath = d3.geoPath(projection);
const polygons = tracts
.append('path')
.attr('class', 'polygon')
.attr('fill', d => this.colorScale(d.properties[this.encodingDefaults.c]))
.attr('data-id', d => d.properties[this.id_var])
.attr('d', geoPath)
.call(stylePolygon);

// LEGEND
const options = Object.assign({
color: this.ordinalLabels.length
? d3.scaleOrdinal(this.ordinalLabels, this.colorBins)
: this.colorScale,
});
const legendSVG = legend(options);
const legendWidth = +d3.select(legendSVG).attr('width');
const legendHeight = +d3.select(legendSVG).attr('height');
const legendNode = svg.append('g')
.attr('transform', `translate(${mapX-80}, ${mapY+400})`);
Array.from(legendSVG.children).forEach((child) => {
legendNode.node().appendChild(child);
});
legendNode.select('g').selectAll('text')
.filter(function() { return d3.select(this).attr('font-weight') === 'bold'; })
.attr('font-size', 14);
tracts
.append('text')
.attr('class', 'label')
.text(d => d.properties[this.id_var])
.attr('x', d => geoPath.centroid(d)[0])
.attr('id', d => `label-${d.properties[this.id_var]}`)
.attr('y', d => geoPath.bounds(d)[0][1] - 3)
.call(styleLabel);

tracts1
.append('text')
.attr('class', 'label')
.text(d => d.properties[this.id_var])
.attr('x', d => geoPath1.centroid(d)[0])
.attr('id', d => `label-${d.properties[this.id_var]}`)
.attr('y', d => geoPath1.bounds(d)[0][1] - 3)
.call(styleLabel);
[[tracts1, d => d.properties], [tracts, d => d.properties]]
.forEach(([els, accessor]) => {
els
.on('mouseover', (data) => {
if (this.lock_active_zip) return;
const zip = accessor(data);
// if (zip.population) {
if (zip[column]) {
clearTimeout(this.mouseoutTimeout);
this.active_zip = zip;
this.update()
}
})
.on('mouseout', () => {
if (this.lock_active_zip) return;
this.mouseoutTimeout = setTimeout(() => {
this.active_zip = undefined;
this.update()
}, 100);
})
.on('click', (data) => {
const zip = accessor(data);
console.log(zip)
if (this.lock_active_zip && this.active_zip === zip) {
this.lock_active_zip = false;
this.active_zip = undefined;
this.neighborTarget = undefined;
this.update()
} else if (zip[column]) {
clearTimeout(this.mouseoutTimeout);
this.lock_active_zip = true;
this.active_zip = zip;
this.neighborTarget = zip.locNeighbors;
this.update()
}
});
});
// const t = svg.transition().duration(this.rendered ? 300 : 0);
// x.domain(d3.extent(data, d => d[this.encoding.x]));
// //[d3.extent(data, d => d[this.encoding.x])[0]-1, d3.extent(data, d => d[this.encoding.x])[1]]
// y.domain(d3.extent(data, d => d[this.encoding.y]));
// //this.colorScale.domain(d3.extent(data, d => d[this.encoding.c]));
// //size.domain(d3.extent(data, d => d[this.encoding.s]));
// voronoi.x(d => x(d[this.encoding.x])).y(d => y(d[this.encoding.y]));
// linear_regression.x(d => d[this.encoding.x]).y(d => d[this.encoding.y]);
// const regression = linear_regression(data);
// xLabel.text(`${column} (Standardized) ⟶`);
// yLabel.text(`Spatially Lagged ${column} ⟶`);
// cells
// .data(voronoi(data).polygons(), d => d && d.data && d.data[id_var])
// .join('g');
// cells.selectAll('path').remove();
// cells
// .append('path')
// .attr('d', d => d && d.join && `M${d.join('L')}Z`)
// .attr('fill', 'none')
// // .attr('stroke', 'red')
// .attr('pointer-events', 'all');
// labels
// .attr('x', d => d && d.data && x(d.data[this.encoding.x]))
// .attr('y', d => d && d.data && y(d.data[this.encoding.y]));
// line
// .datum(regression)
// .attr('x1', d => x(d[0][0]))
// .attr('x2', d => x(d[1][0]))
// .attr('y1', d => y(d[0][1]))
// .attr('y2', d => y(d[1][1]));

// yBase
// .attr('x1', d => x(0))
// .attr('x2', d => x(0))
// .attr('y1', d => y(d3.extent(this.data, d => d[this.encoding.y])[0]))
// .attr('y2', d => y(d3.extent(this.data, d => d[this.encoding.y])[1]));
// xBase
// .attr('x1', d => x(d3.extent(this.data, d => d[this.encoding.x])[0]))
// .attr('x2', d => x(d3.extent(this.data, d => d[this.encoding.x])[1]))
// .attr('y1', d => y(0))
// .attr('y2', d => y(0));
polygons
.attr('fill', d => {
//console.log(this.colorScale(d.properties[this.encoding.c]))
return this.colorScale(d.properties[this.encoding.c])
//d.properties[this.encoding.c])
})
this.rendered = true;
// tracts.attr('class', 'tract').select('text').attr('visibility', 'hidden');
// cells.attr('class', 'cell').select('text').attr('visibility', 'hidden');
// circles.call(styleCircle);
// polygons.call(stylePolygon);
// line.raise();
// if (active_zip) {
// [[cells, 'cell', x => x.data[id_var]], [tracts, 'tract', x => x.properties[id_var]]]
// .forEach(([els, className, accessor]) => els
// .filter(d => {
// return (accessor(d) === active_zip[id_var])
// })
// .attr('class', `${className} active`)
// .raise()
// .filter((_, i) => !i)
// .attr('class', `${className} active`)
// .select('text')
// .attr('visibility', 'visible'));
// cells
// .filter('.active')
// .select('circle')
// .attr('fill-opacity', 1)
// .attr('stroke', '#000')
// .attr('stroke-opacity', 1)
// .attr('stroke-width', 2);
// tracts
// .filter('.active')
// .select('path')
// .attr('stroke', '#000')
// .attr('stroke-width', 4)
// .attr('paint-order', 'stroke');
return Object.assign(html`<div style="position: relative;">${[svg.node()]}</div>`);
}
}
Insert cell
function lngLatToArc(d, sourceLngLat, targetLngLat, bend){
// If no bend is supplied, then do the plain square root
bend = bend || 1;
// `d[sourceName]` and `d[targetname]` are arrays of `[lng, lat]`
// Note, people often put these in lat then lng, but mathematically we want x then y which is `lng,lat`

if (targetLngLat && sourceLngLat) {
let sourceXY = projection( sourceLngLat ),
targetXY = projection( targetLngLat );

let sourceX = sourceXY[0],
sourceY = sourceXY[1];

let targetX = targetXY[0],
targetY = targetXY[1];

let dx = targetX - sourceX,
dy = targetY - sourceY,
dr = Math.sqrt(dx * dx + dy * dy)*5;

//To avoid a whirlpool effect, make the bend direction consistent regardless of whether the source is east or west of the target
let west_of_source = (targetX - sourceX) < 0;
if (west_of_source) return "M" + targetX + "," + targetY + "A" + dr + "," + dr + " 0 0,1 " + sourceX + "," + sourceY;
return "M" + sourceX + "," + sourceY + "A" + dr + "," + dr + " 0 0,1 " + targetX + "," + targetY;
} else {
return "M0,0,l0,0z";
}
}
Insert cell
Insert cell
viewof neighborsMap =
neighborsChoroplethMap({
data: colValues,
geography: geo,
neighbors: neighbors,
topographyBorder: border, // pre-computed border
breaks: breaks_col, // breaks for lisa values
colorBins: breaks_colors,
ordinalLabels: breaks_labels,
strokeColor: '#999999',
colorVariable: column,
geographyIdVariable: id_var,
dataIdVariable: id_var,
legend: {
title: 'See Neighbors',
},
hoverFn: f => neighbors[f]
})
Insert cell
projection(conn.targets[0].position)
Insert cell
conn = geoda.getConnectivity(weights, mapId)
Insert cell
// import {log} from '@sorig/console-log-without-leaving-your-notebook'
Insert cell
import {callout} from '@d3/line-chart-with-tooltip'
Insert cell
import {legend, swatches} from "@d3/color-legend"
Insert cell
function getNeighbors(geojsonData, idCol, weights){
let neighborsObj = {}
// loop through
for (let i=0; i<geojsonData.features.length;i++){
// snag ID column
const currId = geojsonData.features[i].properties[idCol]
// find neighbors
const currNeighbors = geoda.getNeighbors(weights, i)
neighborsObj[currId] = [currId, ...(currNeighbors.map(i => geojsonData.features[i].properties[idCol]))]
}
return neighborsObj
}
Insert cell
neighbors = getNeighbors(geojson, id_var, weights)
Insert cell
attachment_array = FileAttachment("guerry_wgs84@1.geojson").arrayBuffer()
Insert cell
mapId = geoda.readGeoJSON(attachment_array)
Insert cell
weights = geoda.getQueenWeights(mapId)
Insert cell
id_var = 'dept'
Insert cell
column = 'Donatns'
Insert cell
column_selected = geoda.getColumn(mapId, column)
Insert cell
lm = geoda.localMoran(weights, column_selected)
Insert cell
std_vals = standardize(column_selected)
Insert cell
spatial_lags = geoda.spatialLag(weights, std_vals)
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function standardize(arr){ // used to standardize column values as # of stdev away from the mean
const avg = ss.mean(arr);
const stdev = ss.standardDeviation(arr);
return arr.map(val => {
return ss.zScore(val, avg, stdev);
})
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
breaks = geoda.naturalBreaks(6, data.map(o => o[column]))
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
geoda = await jsgeoda.New()
Insert cell
jsgeoda = import('https://cdn.skypack.dev/jsgeoda')
Insert cell
ss = require('simple-statistics')
Insert cell
Insert cell
Insert cell
Insert cell
import { inputsGroup } from '@bumbeishvili/input-groups'
Insert cell
// import { legend } from '@d3/color-legend'
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