如何使用 d3 force space 消除地图上的重叠点
How can I use d3 force to space out overlapping points on a map
我的地图有一些重叠点。我正在使用四叉树和 turf.js 来确定,当我单击一个点时,30 英里半径内还有多少其他点。
我想做的(如果该半径内有多个点)是使用 d3.forceSimulation 平均分布重叠点。
这是我想做的一个非常接近的例子,但是使用了 d3v3 和 google 地图:http://bl.ocks.org/cdmahoney/raw/9876525/?raw=true
我已经包括了 d3.forceSimulation,当我点击一个在 30 英里半径范围内有多个点的地方时,这些点确实会受到力——但它们会向上移动到页面。
如何让点从我在地图上单击的位置均匀地推出,如下所示:
非常感谢帮助!!
let margin = { top: 0, right: 0, bottom: 10, left: 0 },
width = 1000 - margin.left - margin.right,
height = 800 - margin.top - margin.bottom;
let projection = geoAlbersUsaTerritories.geoAlbersUsaTerritories()
.scale(width)
.translate([width / 2, height / 2.2]);
const path = d3.geoPath()
.projection(projection);
var simulation = d3.forceSimulation()
.force('charge', d3.forceManyBody().strength(-160))
.stop()
let eventX,
eventY
const formatDate2 = d3.timeFormat("%m-%Y")
const svg = d3.select("#content")
.append("svg")
.attr('id', 'map')
.style("width", width + margin.left + margin.right)
.style("height", height + margin.top + margin.bottom)
const map = svg.append('g')
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.attr('class', 'map')
const landmass = map.append('g').attr('class', 'land')
const places = map.append('g').attr('id', 'places')
svg.append('ellipse').classed("radius", true).attr('id', 'locate')
d3.json("https://unpkg.com/us-atlas@3.0.0/states-10m.json").then(function (usa) {
landmass.selectAll('path')
.data(topojson.feature(usa, usa.objects.nation).features)
.enter().append("path")
.attr("d", path)
.attr("class", "outline")
.attr('fill', '#ccc')
.attr("stroke", "#999")
landmass.append("path")
.datum(topojson.mesh(usa, usa.objects.states, function (a, b) { return a !== b; }))
.attr("class", "mesh")
.attr("d", path)
.attr('fill', 'none')
.attr('stroke', 'white')
});
const data = [
{
"id": 3448,
"name": "General Edward Lawrence Logan Intl",
"city": "Boston",
"faa": "BOS",
"latitude": 42.364347,
"longitude": -71.005181
},
{
"id": 3453,
"name": "Metropolitan Oakland Intl",
"city": "Oakland",
"faa": "OAK",
"latitude": 37.721278,
"longitude": -122.220722
},
{
"id": 3454,
"name": "Eppley Afld",
"city": "Omaha",
"faa": "OMA",
"latitude": 41.303167,
"longitude": -95.894069
},
{
"id": 3457,
"name": "Wichita Mid Continent",
"city": "Wichita",
"faa": "ICT",
"latitude": 37.649944,
"longitude": -97.433056
},
{
"id": 3458,
"name": "Kansas City Intl",
"city": "Kansas City",
"faa": "MCI",
"latitude": 39.297606,
"longitude": -94.713905
},
{
"id": 3459,
"name": "Dane Co Rgnl Truax Fld",
"city": "Madison",
"faa": "MSN",
"latitude": 43.139858,
"longitude": -89.337514
},
{
"id": 3462,
"name": "Phoenix Sky Harbor Intl",
"city": "Phoenix",
"faa": "PHX",
"latitude": 33.434278,
"longitude": -112.011583
},
{
"id": 3467,
"name": "Spokane Intl",
"city": "Spokane",
"faa": "GEG",
"latitude": 47.619861,
"longitude": -117.533833
},
{
"id": 3469,
"name": "San Francisco Intl",
"city": "San Francisco",
"faa": "SFO",
"latitude": 37.618972,
"longitude": -122.374889
},
{
"id": 3472,
"name": "Gainesville Rgnl",
"city": "Gainesville",
"faa": "GNV",
"latitude": 29.690056,
"longitude": -82.271778
},
{
"id": 3473,
"name": "Memphis Intl",
"city": "Memphis",
"faa": "MEM",
"latitude": 35.042417,
"longitude": -89.976667
},
{
"id": 3484,
"name": "Los Angeles Intl",
"city": "Los Angeles",
"faa": "LAX",
"latitude": 33.942536,
"longitude": -118.408075
},
{
"id": 3486,
"name": "Cleveland Hopkins Intl",
"city": "Cleveland",
"faa": "CLE",
"latitude": 41.411689,
"longitude": -81.849794
},
{
"id": 3494,
"name": "Newark Liberty Intl",
"city": "Newark",
"faa": "EWR",
"latitude": 40.6925,
"longitude": -74.168667
},
{
"id": 3502,
"name": "Dallas Love Fld",
"city": "Dallas",
"faa": "DAL",
"latitude": 32.847111,
"longitude": -96.851778
},
{
"id": 3550,
"name": "George Bush Intercontinental",
"city": "Houston",
"faa": "IAH",
"latitude": 29.984433,
"longitude": -95.341442
},
{
"id": 3559,
"name": "El Paso Intl",
"city": "El Paso",
"faa": "ELP",
"latitude": 31.80725,
"longitude": -106.377583
},
{
"id": 3566,
"name": "William P Hobby",
"city": "Houston",
"faa": "HOU",
"latitude": 29.645419,
"longitude": -95.278889
},
{
"id": 3570,
"name": "Pittsburgh Intl",
"city": "Pittsburgh",
"faa": "PIT",
"latitude": 40.491467,
"longitude": -80.232872
},
{
"id": 3576,
"name": "Miami Intl",
"city": "Miami",
"faa": "MIA",
"latitude": 25.79325,
"longitude": -80.290556
},
{
"id": 3582,
"name": "Long Beach",
"city": "Long Beach",
"faa": "LGB",
"latitude": 33.817722,
"longitude": -118.151611
},
{
"id": 3585,
"name": "Indianapolis Intl",
"city": "Indianapolis",
"faa": "IND",
"latitude": 39.717331,
"longitude": -86.294383
},
{
"id": 3589,
"name": "Westchester Co",
"city": "White Plains",
"faa": "HPN",
"latitude": 41.066959,
"longitude": -73.707575
},
{
"id": 3697,
"name": "La Guardia",
"city": "New York",
"faa": "LGA",
"latitude": 40.777245,
"longitude": -73.872608
},
{
"id": 3747,
"name": "Chicago Midway Intl",
"city": "Chicago",
"faa": "MDW",
"latitude": 41.785972,
"longitude": -87.752417
},
{
"id": 3797,
"name": "John F Kennedy Intl",
"city": "New York",
"faa": "JFK",
"latitude": 40.639751,
"longitude": -73.778925
},
{
"id": 3830,
"name": "Chicago Ohare Intl",
"city": "Chicago",
"faa": "ORD",
"latitude": 41.978603,
"longitude": -87.904842
}
]
d3.selectAll('.close').on('click', function () {
d3.selectAll('.popup').remove()
})
data.forEach(function (d) {
d.latitude = +d.latitude;
d.longitude = +d.longitude;
})
d3.selectAll('.location').remove()
let locations = places.selectAll(".location")
.data(data);
locations.enter()
.append("circle")
.attr('id', d => 'n' + d.id)
.attr("class", 'location')
.attr('cx', d => projection([d.longitude, d.latitude])[0])
.attr('cy', d => projection([d.longitude, d.latitude])[1])
.attr("r", 5)
.attr('fill', 'green')
.style('stroke', '#fff')
.style('stroke-width', .5)
.style("opacity", .75)
.on('click', function (event, d) {
simulation.stop()
var isSelectedCode = d.detention_facility_code
var isSelectedName = d.name
var whichclass = d3.select(this).attr("class").split(' ');
let activeIndex = whichclass.indexOf('active')
var sel = d3.select(this);
sel.raise();
let latlng = [d.longitude, d.latitude]
$('#clickedFacility').text(d.name)
$('#slider').removeClass('hide')
showRadius(latlng, far)
})
.on("mouseover", function (event, d) {
var sel = d3.select(this);
sel.raise();
let tooltip_str = d.name
tooltip.html(tooltip_str)
.style("visibility", "visible");
})
.on("mousemove", function (event, d) {
tooltip.style("top", event.pageY - (tooltip.node().clientHeight + 5) + "px")
.style("left", event.pageX - (tooltip.node().clientWidth / 2.0) + "px");
})
.on("mouseout", function (event, d) {
var sel = d3.select(this);
sel.lower();
tooltip.style("visibility", "hidden");
})
locations
.transition()
.duration(100)
.attr("class", d => "location " + d.name.replace(/[\s]/g, '') + ' ' + d.type_detailed.replace(/\s+|[,\/]/g, "") + ' closed' + d.is_closed)
.attr("r", 5)
.attr('fill', 'green')
locations.exit()
.remove();
const tooltip = d3.select("body").append("div")
.attr("class", "tooltip");
var info = svg.append("div")
.attr("class", "info");
const quadtree = d3.quadtree()
.x(d => +d.longitude)
.y(d => +d.latitude)
.addAll(data);
let miles = 30
let far = (1 / 60) * miles; // in degrees
function showRadius(evt, far) {
d3.select('ellipse.radius').remove();
let radiusCircle = map.append("ellipse").classed("radius", true).attr('id', 'locate')
let xy = projection.invert(evt)
console.log('xy', xy)
let xyObject = { "longitude": evt[0], "latitude": evt[1] }
radiusLng = evt[0] + far
radiusLat = evt[1] + far
radiusLngLat = [+radiusLng, +radiusLat]
radiusPoint = projection(radiusLngLat)
console.log('radiusPoint', radiusPoint)
radiusX = Math.abs(evt[1] + far)
radiusY = Math.abs(evt[0] + far)
radiusToPoint = projection([radiusY, radiusX][0])
d3.select('ellipse.radius').classed('hide', false)
d3.select('ellipse.radius').classed('show', true)
let radiusprojx = projection(evt)[0]
let radiusprojxN = projection(evt)[0] - eventX
let radiusprojy = projection(evt)[1]
let radiusprojyN = projection(evt)[1] - eventY
radiusCircle
.attr('cx', d => (projection(evt)[0]))
.attr('cy', d => (projection(evt)[1]))
.attr('rx', 20)
.attr('ry', 20)
let hits = [];
quadtree.visit(nearest(xyObject, far, hits))
for (i = 0; i < hits.length; i++) {
let line = turf.lineString([[evt[1], evt[0]], [hits[i].latitude, hits[i].longitude]]);
let length = turf.length(line, { units: 'miles' });
hits[i].distance = +length.toFixed(2) + ' miles';
}
hits.sort(function (a, b) { return d3.ascending(a.distance, b.distance) })
let locationsInRadius = hits.map(a => a.id);
console.log('locationsInRadius', locationsInRadius)
d3.selectAll('.location').attr('fill', 'green')
locationsInRadius.forEach(function (d, i) {
d3.select('#n' + d).attr('fill', 'blue')
})
let total_count = hits.length
$("#hitnumber").text(total_count + " airports within 30 miles")
d3.selectAll('.list-item').remove()
let listItem = d3.selectAll('#hits').selectAll('text')
.data(hits)
.attr('padding-left', '20px')
.enter().append('div').attr('class', 'list-item')
.html(d => d.name + "<br/>Lat: " + d.latitude + "<br/>Lng: " + d.longitude + "<br/>Distance: " + d.distance)
if (hits.length == 1) {
} else {
const hitids = []
for (i = 0; i < hits.length; i++) {
hitids.push('#n' + hits[i].id)
}
let idstoget = hitids.toString()
let forceids = d3.selectAll(idstoget)
simulation.force('x', d3.forceX().strength(10).x(radiusPoint[0]))
simulation.force('y', d3.forceY().strength(10).y(radiusPoint[1]))
// simulation.force('center', d3.forceCenter(radiusPoint[0], radiusPoint[1]))
simulation.alpha(1).restart()
simulation.nodes(forceids)
.on('tick', ticked)
function ticked() {
update(forceids)
}
function update(forceids) {
forceids
.attr('cx', function (d) { return d.x })
.attr('cy', function (d) { return d.y })
}
}
}
function nearest(node, radius, hits) {
if (!hits) hits = [];
var r = radius,
nx1 = node.longitude - r,
nx2 = node.longitude + r,
ny1 = node.latitude - r,
ny2 = node.latitude + r;
return function (quad, x1, y1, x2, y2) {
if (quad.data && (quad.data !== node)) {
var x = node.longitude - quad.data.longitude,
y = node.latitude - quad.data.latitude,
l = Math.sqrt(x * x + y * y),
r = radius;
if (l < r) {
hits.push(quad.data)
} else {
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
}
}
.tooltip {
position: absolute;
padding: 15px;
font: 12px sans-serif;
background: #fff;
color: #000;
border: 0px;
pointer-events: none;
opacity: 0.8;
visibility: hidden;
-moz-box-shadow: 0 0 15px #aaa;
-webkit-box-shadow: 0 0 15px #aaa;
box-shadow: 0 0 15px #aaa;
}
.close {
float: right;
margin-top: 1 px;
}
.multiple-choice {
padding: 3px 0;
}
.radius {
fill-opacity: 0.15;
stroke: #333;
stroke-dasharray: 4 2;
z-index: 1000;
fill: #bff4ff;
display: none;
}
#panel {
position: absolute;
left: 1030px;
top: 0px;
width: 300px;
padding-top: 50px;
}
.list-item {
padding: 10px;
}
label {
margin: 0;
padding: 0;
font-family: Arial, Helvetica, sans-serif;
}
#hitnumber {
font-family: Arial, Helvetica, sans-serif;
padding-left: 10px;
}
.hide {
display: none;
}
<script src="https://cdn.jsdelivr.net/npm/d3-quadtree@3"></script>
<script src="https://d3js.org/d3.v6.min.js"></script>
<script src='https://npmcdn.com/@turf/turf/turf.min.js'></script>
<script src="//code.jquery.com/jquery-1.10.2.js"></script>
<script src="https://unpkg.com/topojson-client@3"></script>
<script src="https://unpkg.com/geo-albers-usa-territories@0.1.0/dist/geo-albers-usa-territories.js"></script>
<div id="content"></div>
<div id="panel">
<div id="list">
<div id="hitnumber"></div>
<pre><div id="hits"></div></pre>
</div>
</div>
<div id="chart-title"></div>
虽然我很想不为此使用强制布局,但我将使用您在此处的代码(尽管将圆圈连接到其原始位置的线的问题是此处未解决)并快速解决为什么圆圈的行为不像您预期的那样。
强制布局将在节点上创建适当的属性(如果它们不存在)。对于节点的位置,这些属性是 d.x 和 d.y。您的数据没有 x 或 y 属性,因此当您创建力时,节点会使用原点 [0,0] 周围的值进行初始化,这就是它们迁移到左上角的原因。这个问题可以通过创建 x 和 y 属性来解决:
.each(d=>[d.x,d.y] = projection([d.longitude, d.latitude]))
在下面的代码片段中,我在输入圆圈后立即使用它。
其次,您想将绑定数据传递给力布局而不是节点(否则,由于节点本身没有 x,y 属性,我们将再次在左上角初始化它们)。我们也不想将选择作为节点传递,相反,让我们访问选择的数据:
simulation.nodes(forceids.data())
其中,使用快速而肮脏的几行来重置移动的节点(使用 lat/long,并将 x,y 重置为 lat/long),给我们:
let margin = { top: 0, right: 0, bottom: 10, left: 0 },
width = 1000 - margin.left - margin.right,
height = 800 - margin.top - margin.bottom;
let projection = geoAlbersUsaTerritories.geoAlbersUsaTerritories()
.scale(width)
.translate([width / 2, height / 2.2]);
const path = d3.geoPath()
.projection(projection);
var simulation = d3.forceSimulation()
.force('charge', d3.forceManyBody().strength(-160))
.stop()
let eventX,
eventY
const formatDate2 = d3.timeFormat("%m-%Y")
const svg = d3.select("#content")
.append("svg")
.attr('id', 'map')
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
const map = svg.append('g')
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.attr('class', 'map')
const landmass = map.append('g').attr('class', 'land')
const places = map.append('g').attr('id', 'places')
svg.append('ellipse').classed("radius", true).attr('id', 'locate')
d3.json("https://unpkg.com/us-atlas@3.0.0/states-10m.json").then(function (usa) {
landmass.selectAll('path')
.data(topojson.feature(usa, usa.objects.nation).features)
.enter().append("path")
.attr("d", path)
.attr("class", "outline")
.attr('fill', '#ccc')
.attr("stroke", "#999")
landmass.append("path")
.datum(topojson.mesh(usa, usa.objects.states, function (a, b) { return a !== b; }))
.attr("class", "mesh")
.attr("d", path)
.attr('fill', 'none')
.attr('stroke', 'white')
});
const data = [
{
"id": 3448,
"name": "General Edward Lawrence Logan Intl",
"city": "Boston",
"faa": "BOS",
"latitude": 42.364347,
"longitude": -71.005181
},
{
"id": 3453,
"name": "Metropolitan Oakland Intl",
"city": "Oakland",
"faa": "OAK",
"latitude": 37.721278,
"longitude": -122.220722
},
{
"id": 3454,
"name": "Eppley Afld",
"city": "Omaha",
"faa": "OMA",
"latitude": 41.303167,
"longitude": -95.894069
},
{
"id": 3457,
"name": "Wichita Mid Continent",
"city": "Wichita",
"faa": "ICT",
"latitude": 37.649944,
"longitude": -97.433056
},
{
"id": 3458,
"name": "Kansas City Intl",
"city": "Kansas City",
"faa": "MCI",
"latitude": 39.297606,
"longitude": -94.713905
},
{
"id": 3459,
"name": "Dane Co Rgnl Truax Fld",
"city": "Madison",
"faa": "MSN",
"latitude": 43.139858,
"longitude": -89.337514
},
{
"id": 3462,
"name": "Phoenix Sky Harbor Intl",
"city": "Phoenix",
"faa": "PHX",
"latitude": 33.434278,
"longitude": -112.011583
},
{
"id": 3467,
"name": "Spokane Intl",
"city": "Spokane",
"faa": "GEG",
"latitude": 47.619861,
"longitude": -117.533833
},
{
"id": 3469,
"name": "San Francisco Intl",
"city": "San Francisco",
"faa": "SFO",
"latitude": 37.618972,
"longitude": -122.374889
},
{
"id": 3472,
"name": "Gainesville Rgnl",
"city": "Gainesville",
"faa": "GNV",
"latitude": 29.690056,
"longitude": -82.271778
},
{
"id": 3473,
"name": "Memphis Intl",
"city": "Memphis",
"faa": "MEM",
"latitude": 35.042417,
"longitude": -89.976667
},
{
"id": 3484,
"name": "Los Angeles Intl",
"city": "Los Angeles",
"faa": "LAX",
"latitude": 33.942536,
"longitude": -118.408075
},
{
"id": 3486,
"name": "Cleveland Hopkins Intl",
"city": "Cleveland",
"faa": "CLE",
"latitude": 41.411689,
"longitude": -81.849794
},
{
"id": 3494,
"name": "Newark Liberty Intl",
"city": "Newark",
"faa": "EWR",
"latitude": 40.6925,
"longitude": -74.168667
},
{
"id": 3502,
"name": "Dallas Love Fld",
"city": "Dallas",
"faa": "DAL",
"latitude": 32.847111,
"longitude": -96.851778
},
{
"id": 3550,
"name": "George Bush Intercontinental",
"city": "Houston",
"faa": "IAH",
"latitude": 29.984433,
"longitude": -95.341442
},
{
"id": 3559,
"name": "El Paso Intl",
"city": "El Paso",
"faa": "ELP",
"latitude": 31.80725,
"longitude": -106.377583
},
{
"id": 3566,
"name": "William P Hobby",
"city": "Houston",
"faa": "HOU",
"latitude": 29.645419,
"longitude": -95.278889
},
{
"id": 3570,
"name": "Pittsburgh Intl",
"city": "Pittsburgh",
"faa": "PIT",
"latitude": 40.491467,
"longitude": -80.232872
},
{
"id": 3576,
"name": "Miami Intl",
"city": "Miami",
"faa": "MIA",
"latitude": 25.79325,
"longitude": -80.290556
},
{
"id": 3582,
"name": "Long Beach",
"city": "Long Beach",
"faa": "LGB",
"latitude": 33.817722,
"longitude": -118.151611
},
{
"id": 3585,
"name": "Indianapolis Intl",
"city": "Indianapolis",
"faa": "IND",
"latitude": 39.717331,
"longitude": -86.294383
},
{
"id": 3589,
"name": "Westchester Co",
"city": "White Plains",
"faa": "HPN",
"latitude": 41.066959,
"longitude": -73.707575
},
{
"id": 3697,
"name": "La Guardia",
"city": "New York",
"faa": "LGA",
"latitude": 40.777245,
"longitude": -73.872608
},
{
"id": 3747,
"name": "Chicago Midway Intl",
"city": "Chicago",
"faa": "MDW",
"latitude": 41.785972,
"longitude": -87.752417
},
{
"id": 3797,
"name": "John F Kennedy Intl",
"city": "New York",
"faa": "JFK",
"latitude": 40.639751,
"longitude": -73.778925
},
{
"id": 3830,
"name": "Chicago Ohare Intl",
"city": "Chicago",
"faa": "ORD",
"latitude": 41.978603,
"longitude": -87.904842
}
]
d3.selectAll('.close').on('click', function () {
d3.selectAll('.popup').remove()
})
data.forEach(function (d) {
d.latitude = +d.latitude;
d.longitude = +d.longitude;
})
d3.selectAll('.location').remove()
let locations = places.selectAll(".location")
.data(data);
locations.enter()
.append("circle")
.attr('id', d => 'n' + d.id)
.attr("class", 'location')
.attr('cx', d => projection([d.longitude, d.latitude])[0])
.attr('cy', d => projection([d.longitude, d.latitude])[1])
.each(d=>[d.x,d.y] = projection([d.longitude, d.latitude]))
.attr("r", 5)
.attr('fill', 'green')
.style('stroke', '#fff')
.style('stroke-width', .5)
.style("opacity", .75)
.on('click', function (event, d) {
simulation.stop()
var isSelectedCode = d.detention_facility_code
var isSelectedName = d.name
var whichclass = d3.select(this).attr("class").split(' ');
let activeIndex = whichclass.indexOf('active')
var sel = d3.select(this);
sel.raise();
let latlng = [d.longitude, d.latitude]
$('#clickedFacility').text(d.name)
$('#slider').removeClass('hide')
showRadius(latlng, far)
})
.on("mouseover", function (event, d) {
var sel = d3.select(this);
sel.raise();
let tooltip_str = d.name
tooltip.html(tooltip_str)
.style("visibility", "visible");
})
.on("mousemove", function (event, d) {
tooltip.style("top", event.pageY - (tooltip.node().clientHeight + 5) + "px")
.style("left", event.pageX - (tooltip.node().clientWidth / 2.0) + "px");
})
.on("mouseout", function (event, d) {
var sel = d3.select(this);
sel.lower();
tooltip.style("visibility", "hidden");
})
locations
.transition()
.duration(100)
.attr("class", d => "location " + d.name.replace(/[\s]/g, '') + ' ' + d.type_detailed.replace(/\s+|[,\/]/g, "") + ' closed' + d.is_closed)
.attr("r", 5)
.attr('fill', 'green')
locations.exit()
.remove();
const tooltip = d3.select("body").append("div")
.attr("class", "tooltip");
var info = svg.append("div")
.attr("class", "info");
const quadtree = d3.quadtree()
.x(d => +d.longitude)
.y(d => +d.latitude)
.addAll(data);
let miles = 30
let far = (1 / 60) * miles; // in degrees
function showRadius(evt, far) {
// Reset //
svg.selectAll('circle')
.attr('cx', d => d.x = projection([d.longitude, d.latitude])[0])
.attr('cy', d => d.y = projection([d.longitude, d.latitude])[1])
.attr('fill', 'green')
/////
d3.select('ellipse.radius').remove();
let radiusCircle = map.append("ellipse").classed("radius", true).attr('id', 'locate')
let xy = projection.invert(evt)
console.log('xy', xy)
let xyObject = { "longitude": evt[0], "latitude": evt[1] }
radiusLng = evt[0] + far
radiusLat = evt[1] + far
radiusLngLat = [+radiusLng, +radiusLat]
radiusPoint = projection(radiusLngLat)
console.log('radiusPoint', radiusPoint)
radiusX = Math.abs(evt[1] + far)
radiusY = Math.abs(evt[0] + far)
radiusToPoint = projection([radiusY, radiusX][0])
d3.select('ellipse.radius').classed('hide', false)
d3.select('ellipse.radius').classed('show', true)
let radiusprojx = projection(evt)[0]
let radiusprojxN = projection(evt)[0] - eventX
let radiusprojy = projection(evt)[1]
let radiusprojyN = projection(evt)[1] - eventY
radiusCircle
.attr('cx', d => (projection(evt)[0]))
.attr('cy', d => (projection(evt)[1]))
.attr('rx', 20)
.attr('ry', 20)
let hits = [];
quadtree.visit(nearest(xyObject, far, hits))
for (i = 0; i < hits.length; i++) {
let line = turf.lineString([[evt[1], evt[0]], [hits[i].latitude, hits[i].longitude]]);
let length = turf.length(line, { units: 'miles' });
hits[i].distance = +length.toFixed(2) + ' miles';
}
hits.sort(function (a, b) { return d3.ascending(a.distance, b.distance) })
let locationsInRadius = hits.map(a => a.id);
//console.log('locationsInRadius', locationsInRadius)
d3.selectAll('.location').attr('fill', 'green')
locationsInRadius.forEach(function (d, i) {
d3.select('#n' + d).attr('fill', 'blue')
})
let total_count = hits.length
$("#hitnumber").text(total_count + " airports within 30 miles")
d3.selectAll('.list-item').remove()
let listItem = d3.selectAll('#hits').selectAll('text')
.data(hits)
.attr('padding-left', '20px')
.enter().append('div').attr('class', 'list-item')
.html(d => d.name + "<br/>Lat: " + d.latitude + "<br/>Lng: " + d.longitude + "<br/>Distance: " + d.distance)
if (hits.length == 1) {
} else {
const hitids = []
for (i = 0; i < hits.length; i++) {
hitids.push('#n' + hits[i].id)
}
let idstoget = hitids.toString()
let forceids = d3.selectAll(idstoget)
simulation.force('x', d3.forceX().strength(0.1).x(radiusPoint[0]))
simulation.force('y', d3.forceY().strength(0.1).y(radiusPoint[1]))
// simulation.force('center', d3.forceCenter(radiusPoint[0], radiusPoint[1]))
simulation.alpha(1).restart()
simulation.nodes(forceids.data())
.on('tick', ticked)
function ticked() {
update(forceids)
}
function update(forceids) {
forceids
.attr('cx', function (d) { return d.x })
.attr('cy', function (d) { return d.y })
}
}
}
function nearest(node, radius, hits) {
if (!hits) hits = [];
var r = radius,
nx1 = node.longitude - r,
nx2 = node.longitude + r,
ny1 = node.latitude - r,
ny2 = node.latitude + r;
return function (quad, x1, y1, x2, y2) {
if (quad.data && (quad.data !== node)) {
var x = node.longitude - quad.data.longitude,
y = node.latitude - quad.data.latitude,
l = Math.sqrt(x * x + y * y),
r = radius;
if (l < r) {
hits.push(quad.data)
} else {
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
}
}
.tooltip {
position: absolute;
padding: 15px;
font: 12px sans-serif;
background: #fff;
color: #000;
border: 0px;
pointer-events: none;
opacity: 0.8;
visibility: hidden;
-moz-box-shadow: 0 0 15px #aaa;
-webkit-box-shadow: 0 0 15px #aaa;
box-shadow: 0 0 15px #aaa;
}
.close {
float: right;
margin-top: 1 px;
}
.multiple-choice {
padding: 3px 0;
}
.radius {
fill-opacity: 0.15;
stroke: #333;
stroke-dasharray: 4 2;
z-index: 1000;
fill: #bff4ff;
display: none;
}
#panel {
position: absolute;
left: 1030px;
top: 0px;
width: 300px;
padding-top: 50px;
}
.list-item {
padding: 10px;
}
label {
margin: 0;
padding: 0;
font-family: Arial, Helvetica, sans-serif;
}
#hitnumber {
font-family: Arial, Helvetica, sans-serif;
padding-left: 10px;
}
.hide {
display: none;
}
<script src="https://cdn.jsdelivr.net/npm/d3-quadtree@3"></script>
<script src="https://d3js.org/d3.v6.min.js"></script>
<script src='https://npmcdn.com/@turf/turf/turf.min.js'></script>
<script src="//code.jquery.com/jquery-1.10.2.js"></script>
<script src="https://unpkg.com/topojson-client@3"></script>
<script src="https://unpkg.com/geo-albers-usa-territories@0.1.0/dist/geo-albers-usa-territories.js"></script>
<div id="content"></div>
<div id="panel">
<div id="list">
<div id="hitnumber"></div>
<pre><div id="hits"></div></pre>
</div>
</div>
<div id="chart-title"></div>
我的地图有一些重叠点。我正在使用四叉树和 turf.js 来确定,当我单击一个点时,30 英里半径内还有多少其他点。
我想做的(如果该半径内有多个点)是使用 d3.forceSimulation 平均分布重叠点。
这是我想做的一个非常接近的例子,但是使用了 d3v3 和 google 地图:http://bl.ocks.org/cdmahoney/raw/9876525/?raw=true
我已经包括了 d3.forceSimulation,当我点击一个在 30 英里半径范围内有多个点的地方时,这些点确实会受到力——但它们会向上移动到页面。
如何让点从我在地图上单击的位置均匀地推出,如下所示:
非常感谢帮助!!
let margin = { top: 0, right: 0, bottom: 10, left: 0 },
width = 1000 - margin.left - margin.right,
height = 800 - margin.top - margin.bottom;
let projection = geoAlbersUsaTerritories.geoAlbersUsaTerritories()
.scale(width)
.translate([width / 2, height / 2.2]);
const path = d3.geoPath()
.projection(projection);
var simulation = d3.forceSimulation()
.force('charge', d3.forceManyBody().strength(-160))
.stop()
let eventX,
eventY
const formatDate2 = d3.timeFormat("%m-%Y")
const svg = d3.select("#content")
.append("svg")
.attr('id', 'map')
.style("width", width + margin.left + margin.right)
.style("height", height + margin.top + margin.bottom)
const map = svg.append('g')
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.attr('class', 'map')
const landmass = map.append('g').attr('class', 'land')
const places = map.append('g').attr('id', 'places')
svg.append('ellipse').classed("radius", true).attr('id', 'locate')
d3.json("https://unpkg.com/us-atlas@3.0.0/states-10m.json").then(function (usa) {
landmass.selectAll('path')
.data(topojson.feature(usa, usa.objects.nation).features)
.enter().append("path")
.attr("d", path)
.attr("class", "outline")
.attr('fill', '#ccc')
.attr("stroke", "#999")
landmass.append("path")
.datum(topojson.mesh(usa, usa.objects.states, function (a, b) { return a !== b; }))
.attr("class", "mesh")
.attr("d", path)
.attr('fill', 'none')
.attr('stroke', 'white')
});
const data = [
{
"id": 3448,
"name": "General Edward Lawrence Logan Intl",
"city": "Boston",
"faa": "BOS",
"latitude": 42.364347,
"longitude": -71.005181
},
{
"id": 3453,
"name": "Metropolitan Oakland Intl",
"city": "Oakland",
"faa": "OAK",
"latitude": 37.721278,
"longitude": -122.220722
},
{
"id": 3454,
"name": "Eppley Afld",
"city": "Omaha",
"faa": "OMA",
"latitude": 41.303167,
"longitude": -95.894069
},
{
"id": 3457,
"name": "Wichita Mid Continent",
"city": "Wichita",
"faa": "ICT",
"latitude": 37.649944,
"longitude": -97.433056
},
{
"id": 3458,
"name": "Kansas City Intl",
"city": "Kansas City",
"faa": "MCI",
"latitude": 39.297606,
"longitude": -94.713905
},
{
"id": 3459,
"name": "Dane Co Rgnl Truax Fld",
"city": "Madison",
"faa": "MSN",
"latitude": 43.139858,
"longitude": -89.337514
},
{
"id": 3462,
"name": "Phoenix Sky Harbor Intl",
"city": "Phoenix",
"faa": "PHX",
"latitude": 33.434278,
"longitude": -112.011583
},
{
"id": 3467,
"name": "Spokane Intl",
"city": "Spokane",
"faa": "GEG",
"latitude": 47.619861,
"longitude": -117.533833
},
{
"id": 3469,
"name": "San Francisco Intl",
"city": "San Francisco",
"faa": "SFO",
"latitude": 37.618972,
"longitude": -122.374889
},
{
"id": 3472,
"name": "Gainesville Rgnl",
"city": "Gainesville",
"faa": "GNV",
"latitude": 29.690056,
"longitude": -82.271778
},
{
"id": 3473,
"name": "Memphis Intl",
"city": "Memphis",
"faa": "MEM",
"latitude": 35.042417,
"longitude": -89.976667
},
{
"id": 3484,
"name": "Los Angeles Intl",
"city": "Los Angeles",
"faa": "LAX",
"latitude": 33.942536,
"longitude": -118.408075
},
{
"id": 3486,
"name": "Cleveland Hopkins Intl",
"city": "Cleveland",
"faa": "CLE",
"latitude": 41.411689,
"longitude": -81.849794
},
{
"id": 3494,
"name": "Newark Liberty Intl",
"city": "Newark",
"faa": "EWR",
"latitude": 40.6925,
"longitude": -74.168667
},
{
"id": 3502,
"name": "Dallas Love Fld",
"city": "Dallas",
"faa": "DAL",
"latitude": 32.847111,
"longitude": -96.851778
},
{
"id": 3550,
"name": "George Bush Intercontinental",
"city": "Houston",
"faa": "IAH",
"latitude": 29.984433,
"longitude": -95.341442
},
{
"id": 3559,
"name": "El Paso Intl",
"city": "El Paso",
"faa": "ELP",
"latitude": 31.80725,
"longitude": -106.377583
},
{
"id": 3566,
"name": "William P Hobby",
"city": "Houston",
"faa": "HOU",
"latitude": 29.645419,
"longitude": -95.278889
},
{
"id": 3570,
"name": "Pittsburgh Intl",
"city": "Pittsburgh",
"faa": "PIT",
"latitude": 40.491467,
"longitude": -80.232872
},
{
"id": 3576,
"name": "Miami Intl",
"city": "Miami",
"faa": "MIA",
"latitude": 25.79325,
"longitude": -80.290556
},
{
"id": 3582,
"name": "Long Beach",
"city": "Long Beach",
"faa": "LGB",
"latitude": 33.817722,
"longitude": -118.151611
},
{
"id": 3585,
"name": "Indianapolis Intl",
"city": "Indianapolis",
"faa": "IND",
"latitude": 39.717331,
"longitude": -86.294383
},
{
"id": 3589,
"name": "Westchester Co",
"city": "White Plains",
"faa": "HPN",
"latitude": 41.066959,
"longitude": -73.707575
},
{
"id": 3697,
"name": "La Guardia",
"city": "New York",
"faa": "LGA",
"latitude": 40.777245,
"longitude": -73.872608
},
{
"id": 3747,
"name": "Chicago Midway Intl",
"city": "Chicago",
"faa": "MDW",
"latitude": 41.785972,
"longitude": -87.752417
},
{
"id": 3797,
"name": "John F Kennedy Intl",
"city": "New York",
"faa": "JFK",
"latitude": 40.639751,
"longitude": -73.778925
},
{
"id": 3830,
"name": "Chicago Ohare Intl",
"city": "Chicago",
"faa": "ORD",
"latitude": 41.978603,
"longitude": -87.904842
}
]
d3.selectAll('.close').on('click', function () {
d3.selectAll('.popup').remove()
})
data.forEach(function (d) {
d.latitude = +d.latitude;
d.longitude = +d.longitude;
})
d3.selectAll('.location').remove()
let locations = places.selectAll(".location")
.data(data);
locations.enter()
.append("circle")
.attr('id', d => 'n' + d.id)
.attr("class", 'location')
.attr('cx', d => projection([d.longitude, d.latitude])[0])
.attr('cy', d => projection([d.longitude, d.latitude])[1])
.attr("r", 5)
.attr('fill', 'green')
.style('stroke', '#fff')
.style('stroke-width', .5)
.style("opacity", .75)
.on('click', function (event, d) {
simulation.stop()
var isSelectedCode = d.detention_facility_code
var isSelectedName = d.name
var whichclass = d3.select(this).attr("class").split(' ');
let activeIndex = whichclass.indexOf('active')
var sel = d3.select(this);
sel.raise();
let latlng = [d.longitude, d.latitude]
$('#clickedFacility').text(d.name)
$('#slider').removeClass('hide')
showRadius(latlng, far)
})
.on("mouseover", function (event, d) {
var sel = d3.select(this);
sel.raise();
let tooltip_str = d.name
tooltip.html(tooltip_str)
.style("visibility", "visible");
})
.on("mousemove", function (event, d) {
tooltip.style("top", event.pageY - (tooltip.node().clientHeight + 5) + "px")
.style("left", event.pageX - (tooltip.node().clientWidth / 2.0) + "px");
})
.on("mouseout", function (event, d) {
var sel = d3.select(this);
sel.lower();
tooltip.style("visibility", "hidden");
})
locations
.transition()
.duration(100)
.attr("class", d => "location " + d.name.replace(/[\s]/g, '') + ' ' + d.type_detailed.replace(/\s+|[,\/]/g, "") + ' closed' + d.is_closed)
.attr("r", 5)
.attr('fill', 'green')
locations.exit()
.remove();
const tooltip = d3.select("body").append("div")
.attr("class", "tooltip");
var info = svg.append("div")
.attr("class", "info");
const quadtree = d3.quadtree()
.x(d => +d.longitude)
.y(d => +d.latitude)
.addAll(data);
let miles = 30
let far = (1 / 60) * miles; // in degrees
function showRadius(evt, far) {
d3.select('ellipse.radius').remove();
let radiusCircle = map.append("ellipse").classed("radius", true).attr('id', 'locate')
let xy = projection.invert(evt)
console.log('xy', xy)
let xyObject = { "longitude": evt[0], "latitude": evt[1] }
radiusLng = evt[0] + far
radiusLat = evt[1] + far
radiusLngLat = [+radiusLng, +radiusLat]
radiusPoint = projection(radiusLngLat)
console.log('radiusPoint', radiusPoint)
radiusX = Math.abs(evt[1] + far)
radiusY = Math.abs(evt[0] + far)
radiusToPoint = projection([radiusY, radiusX][0])
d3.select('ellipse.radius').classed('hide', false)
d3.select('ellipse.radius').classed('show', true)
let radiusprojx = projection(evt)[0]
let radiusprojxN = projection(evt)[0] - eventX
let radiusprojy = projection(evt)[1]
let radiusprojyN = projection(evt)[1] - eventY
radiusCircle
.attr('cx', d => (projection(evt)[0]))
.attr('cy', d => (projection(evt)[1]))
.attr('rx', 20)
.attr('ry', 20)
let hits = [];
quadtree.visit(nearest(xyObject, far, hits))
for (i = 0; i < hits.length; i++) {
let line = turf.lineString([[evt[1], evt[0]], [hits[i].latitude, hits[i].longitude]]);
let length = turf.length(line, { units: 'miles' });
hits[i].distance = +length.toFixed(2) + ' miles';
}
hits.sort(function (a, b) { return d3.ascending(a.distance, b.distance) })
let locationsInRadius = hits.map(a => a.id);
console.log('locationsInRadius', locationsInRadius)
d3.selectAll('.location').attr('fill', 'green')
locationsInRadius.forEach(function (d, i) {
d3.select('#n' + d).attr('fill', 'blue')
})
let total_count = hits.length
$("#hitnumber").text(total_count + " airports within 30 miles")
d3.selectAll('.list-item').remove()
let listItem = d3.selectAll('#hits').selectAll('text')
.data(hits)
.attr('padding-left', '20px')
.enter().append('div').attr('class', 'list-item')
.html(d => d.name + "<br/>Lat: " + d.latitude + "<br/>Lng: " + d.longitude + "<br/>Distance: " + d.distance)
if (hits.length == 1) {
} else {
const hitids = []
for (i = 0; i < hits.length; i++) {
hitids.push('#n' + hits[i].id)
}
let idstoget = hitids.toString()
let forceids = d3.selectAll(idstoget)
simulation.force('x', d3.forceX().strength(10).x(radiusPoint[0]))
simulation.force('y', d3.forceY().strength(10).y(radiusPoint[1]))
// simulation.force('center', d3.forceCenter(radiusPoint[0], radiusPoint[1]))
simulation.alpha(1).restart()
simulation.nodes(forceids)
.on('tick', ticked)
function ticked() {
update(forceids)
}
function update(forceids) {
forceids
.attr('cx', function (d) { return d.x })
.attr('cy', function (d) { return d.y })
}
}
}
function nearest(node, radius, hits) {
if (!hits) hits = [];
var r = radius,
nx1 = node.longitude - r,
nx2 = node.longitude + r,
ny1 = node.latitude - r,
ny2 = node.latitude + r;
return function (quad, x1, y1, x2, y2) {
if (quad.data && (quad.data !== node)) {
var x = node.longitude - quad.data.longitude,
y = node.latitude - quad.data.latitude,
l = Math.sqrt(x * x + y * y),
r = radius;
if (l < r) {
hits.push(quad.data)
} else {
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
}
}
.tooltip {
position: absolute;
padding: 15px;
font: 12px sans-serif;
background: #fff;
color: #000;
border: 0px;
pointer-events: none;
opacity: 0.8;
visibility: hidden;
-moz-box-shadow: 0 0 15px #aaa;
-webkit-box-shadow: 0 0 15px #aaa;
box-shadow: 0 0 15px #aaa;
}
.close {
float: right;
margin-top: 1 px;
}
.multiple-choice {
padding: 3px 0;
}
.radius {
fill-opacity: 0.15;
stroke: #333;
stroke-dasharray: 4 2;
z-index: 1000;
fill: #bff4ff;
display: none;
}
#panel {
position: absolute;
left: 1030px;
top: 0px;
width: 300px;
padding-top: 50px;
}
.list-item {
padding: 10px;
}
label {
margin: 0;
padding: 0;
font-family: Arial, Helvetica, sans-serif;
}
#hitnumber {
font-family: Arial, Helvetica, sans-serif;
padding-left: 10px;
}
.hide {
display: none;
}
<script src="https://cdn.jsdelivr.net/npm/d3-quadtree@3"></script>
<script src="https://d3js.org/d3.v6.min.js"></script>
<script src='https://npmcdn.com/@turf/turf/turf.min.js'></script>
<script src="//code.jquery.com/jquery-1.10.2.js"></script>
<script src="https://unpkg.com/topojson-client@3"></script>
<script src="https://unpkg.com/geo-albers-usa-territories@0.1.0/dist/geo-albers-usa-territories.js"></script>
<div id="content"></div>
<div id="panel">
<div id="list">
<div id="hitnumber"></div>
<pre><div id="hits"></div></pre>
</div>
</div>
<div id="chart-title"></div>
虽然我很想不为此使用强制布局,但我将使用您在此处的代码(尽管将圆圈连接到其原始位置的线的问题是此处未解决)并快速解决为什么圆圈的行为不像您预期的那样。
强制布局将在节点上创建适当的属性(如果它们不存在)。对于节点的位置,这些属性是 d.x 和 d.y。您的数据没有 x 或 y 属性,因此当您创建力时,节点会使用原点 [0,0] 周围的值进行初始化,这就是它们迁移到左上角的原因。这个问题可以通过创建 x 和 y 属性来解决:
.each(d=>[d.x,d.y] = projection([d.longitude, d.latitude]))
在下面的代码片段中,我在输入圆圈后立即使用它。
其次,您想将绑定数据传递给力布局而不是节点(否则,由于节点本身没有 x,y 属性,我们将再次在左上角初始化它们)。我们也不想将选择作为节点传递,相反,让我们访问选择的数据:
simulation.nodes(forceids.data())
其中,使用快速而肮脏的几行来重置移动的节点(使用 lat/long,并将 x,y 重置为 lat/long),给我们:
let margin = { top: 0, right: 0, bottom: 10, left: 0 },
width = 1000 - margin.left - margin.right,
height = 800 - margin.top - margin.bottom;
let projection = geoAlbersUsaTerritories.geoAlbersUsaTerritories()
.scale(width)
.translate([width / 2, height / 2.2]);
const path = d3.geoPath()
.projection(projection);
var simulation = d3.forceSimulation()
.force('charge', d3.forceManyBody().strength(-160))
.stop()
let eventX,
eventY
const formatDate2 = d3.timeFormat("%m-%Y")
const svg = d3.select("#content")
.append("svg")
.attr('id', 'map')
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
const map = svg.append('g')
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.attr('class', 'map')
const landmass = map.append('g').attr('class', 'land')
const places = map.append('g').attr('id', 'places')
svg.append('ellipse').classed("radius", true).attr('id', 'locate')
d3.json("https://unpkg.com/us-atlas@3.0.0/states-10m.json").then(function (usa) {
landmass.selectAll('path')
.data(topojson.feature(usa, usa.objects.nation).features)
.enter().append("path")
.attr("d", path)
.attr("class", "outline")
.attr('fill', '#ccc')
.attr("stroke", "#999")
landmass.append("path")
.datum(topojson.mesh(usa, usa.objects.states, function (a, b) { return a !== b; }))
.attr("class", "mesh")
.attr("d", path)
.attr('fill', 'none')
.attr('stroke', 'white')
});
const data = [
{
"id": 3448,
"name": "General Edward Lawrence Logan Intl",
"city": "Boston",
"faa": "BOS",
"latitude": 42.364347,
"longitude": -71.005181
},
{
"id": 3453,
"name": "Metropolitan Oakland Intl",
"city": "Oakland",
"faa": "OAK",
"latitude": 37.721278,
"longitude": -122.220722
},
{
"id": 3454,
"name": "Eppley Afld",
"city": "Omaha",
"faa": "OMA",
"latitude": 41.303167,
"longitude": -95.894069
},
{
"id": 3457,
"name": "Wichita Mid Continent",
"city": "Wichita",
"faa": "ICT",
"latitude": 37.649944,
"longitude": -97.433056
},
{
"id": 3458,
"name": "Kansas City Intl",
"city": "Kansas City",
"faa": "MCI",
"latitude": 39.297606,
"longitude": -94.713905
},
{
"id": 3459,
"name": "Dane Co Rgnl Truax Fld",
"city": "Madison",
"faa": "MSN",
"latitude": 43.139858,
"longitude": -89.337514
},
{
"id": 3462,
"name": "Phoenix Sky Harbor Intl",
"city": "Phoenix",
"faa": "PHX",
"latitude": 33.434278,
"longitude": -112.011583
},
{
"id": 3467,
"name": "Spokane Intl",
"city": "Spokane",
"faa": "GEG",
"latitude": 47.619861,
"longitude": -117.533833
},
{
"id": 3469,
"name": "San Francisco Intl",
"city": "San Francisco",
"faa": "SFO",
"latitude": 37.618972,
"longitude": -122.374889
},
{
"id": 3472,
"name": "Gainesville Rgnl",
"city": "Gainesville",
"faa": "GNV",
"latitude": 29.690056,
"longitude": -82.271778
},
{
"id": 3473,
"name": "Memphis Intl",
"city": "Memphis",
"faa": "MEM",
"latitude": 35.042417,
"longitude": -89.976667
},
{
"id": 3484,
"name": "Los Angeles Intl",
"city": "Los Angeles",
"faa": "LAX",
"latitude": 33.942536,
"longitude": -118.408075
},
{
"id": 3486,
"name": "Cleveland Hopkins Intl",
"city": "Cleveland",
"faa": "CLE",
"latitude": 41.411689,
"longitude": -81.849794
},
{
"id": 3494,
"name": "Newark Liberty Intl",
"city": "Newark",
"faa": "EWR",
"latitude": 40.6925,
"longitude": -74.168667
},
{
"id": 3502,
"name": "Dallas Love Fld",
"city": "Dallas",
"faa": "DAL",
"latitude": 32.847111,
"longitude": -96.851778
},
{
"id": 3550,
"name": "George Bush Intercontinental",
"city": "Houston",
"faa": "IAH",
"latitude": 29.984433,
"longitude": -95.341442
},
{
"id": 3559,
"name": "El Paso Intl",
"city": "El Paso",
"faa": "ELP",
"latitude": 31.80725,
"longitude": -106.377583
},
{
"id": 3566,
"name": "William P Hobby",
"city": "Houston",
"faa": "HOU",
"latitude": 29.645419,
"longitude": -95.278889
},
{
"id": 3570,
"name": "Pittsburgh Intl",
"city": "Pittsburgh",
"faa": "PIT",
"latitude": 40.491467,
"longitude": -80.232872
},
{
"id": 3576,
"name": "Miami Intl",
"city": "Miami",
"faa": "MIA",
"latitude": 25.79325,
"longitude": -80.290556
},
{
"id": 3582,
"name": "Long Beach",
"city": "Long Beach",
"faa": "LGB",
"latitude": 33.817722,
"longitude": -118.151611
},
{
"id": 3585,
"name": "Indianapolis Intl",
"city": "Indianapolis",
"faa": "IND",
"latitude": 39.717331,
"longitude": -86.294383
},
{
"id": 3589,
"name": "Westchester Co",
"city": "White Plains",
"faa": "HPN",
"latitude": 41.066959,
"longitude": -73.707575
},
{
"id": 3697,
"name": "La Guardia",
"city": "New York",
"faa": "LGA",
"latitude": 40.777245,
"longitude": -73.872608
},
{
"id": 3747,
"name": "Chicago Midway Intl",
"city": "Chicago",
"faa": "MDW",
"latitude": 41.785972,
"longitude": -87.752417
},
{
"id": 3797,
"name": "John F Kennedy Intl",
"city": "New York",
"faa": "JFK",
"latitude": 40.639751,
"longitude": -73.778925
},
{
"id": 3830,
"name": "Chicago Ohare Intl",
"city": "Chicago",
"faa": "ORD",
"latitude": 41.978603,
"longitude": -87.904842
}
]
d3.selectAll('.close').on('click', function () {
d3.selectAll('.popup').remove()
})
data.forEach(function (d) {
d.latitude = +d.latitude;
d.longitude = +d.longitude;
})
d3.selectAll('.location').remove()
let locations = places.selectAll(".location")
.data(data);
locations.enter()
.append("circle")
.attr('id', d => 'n' + d.id)
.attr("class", 'location')
.attr('cx', d => projection([d.longitude, d.latitude])[0])
.attr('cy', d => projection([d.longitude, d.latitude])[1])
.each(d=>[d.x,d.y] = projection([d.longitude, d.latitude]))
.attr("r", 5)
.attr('fill', 'green')
.style('stroke', '#fff')
.style('stroke-width', .5)
.style("opacity", .75)
.on('click', function (event, d) {
simulation.stop()
var isSelectedCode = d.detention_facility_code
var isSelectedName = d.name
var whichclass = d3.select(this).attr("class").split(' ');
let activeIndex = whichclass.indexOf('active')
var sel = d3.select(this);
sel.raise();
let latlng = [d.longitude, d.latitude]
$('#clickedFacility').text(d.name)
$('#slider').removeClass('hide')
showRadius(latlng, far)
})
.on("mouseover", function (event, d) {
var sel = d3.select(this);
sel.raise();
let tooltip_str = d.name
tooltip.html(tooltip_str)
.style("visibility", "visible");
})
.on("mousemove", function (event, d) {
tooltip.style("top", event.pageY - (tooltip.node().clientHeight + 5) + "px")
.style("left", event.pageX - (tooltip.node().clientWidth / 2.0) + "px");
})
.on("mouseout", function (event, d) {
var sel = d3.select(this);
sel.lower();
tooltip.style("visibility", "hidden");
})
locations
.transition()
.duration(100)
.attr("class", d => "location " + d.name.replace(/[\s]/g, '') + ' ' + d.type_detailed.replace(/\s+|[,\/]/g, "") + ' closed' + d.is_closed)
.attr("r", 5)
.attr('fill', 'green')
locations.exit()
.remove();
const tooltip = d3.select("body").append("div")
.attr("class", "tooltip");
var info = svg.append("div")
.attr("class", "info");
const quadtree = d3.quadtree()
.x(d => +d.longitude)
.y(d => +d.latitude)
.addAll(data);
let miles = 30
let far = (1 / 60) * miles; // in degrees
function showRadius(evt, far) {
// Reset //
svg.selectAll('circle')
.attr('cx', d => d.x = projection([d.longitude, d.latitude])[0])
.attr('cy', d => d.y = projection([d.longitude, d.latitude])[1])
.attr('fill', 'green')
/////
d3.select('ellipse.radius').remove();
let radiusCircle = map.append("ellipse").classed("radius", true).attr('id', 'locate')
let xy = projection.invert(evt)
console.log('xy', xy)
let xyObject = { "longitude": evt[0], "latitude": evt[1] }
radiusLng = evt[0] + far
radiusLat = evt[1] + far
radiusLngLat = [+radiusLng, +radiusLat]
radiusPoint = projection(radiusLngLat)
console.log('radiusPoint', radiusPoint)
radiusX = Math.abs(evt[1] + far)
radiusY = Math.abs(evt[0] + far)
radiusToPoint = projection([radiusY, radiusX][0])
d3.select('ellipse.radius').classed('hide', false)
d3.select('ellipse.radius').classed('show', true)
let radiusprojx = projection(evt)[0]
let radiusprojxN = projection(evt)[0] - eventX
let radiusprojy = projection(evt)[1]
let radiusprojyN = projection(evt)[1] - eventY
radiusCircle
.attr('cx', d => (projection(evt)[0]))
.attr('cy', d => (projection(evt)[1]))
.attr('rx', 20)
.attr('ry', 20)
let hits = [];
quadtree.visit(nearest(xyObject, far, hits))
for (i = 0; i < hits.length; i++) {
let line = turf.lineString([[evt[1], evt[0]], [hits[i].latitude, hits[i].longitude]]);
let length = turf.length(line, { units: 'miles' });
hits[i].distance = +length.toFixed(2) + ' miles';
}
hits.sort(function (a, b) { return d3.ascending(a.distance, b.distance) })
let locationsInRadius = hits.map(a => a.id);
//console.log('locationsInRadius', locationsInRadius)
d3.selectAll('.location').attr('fill', 'green')
locationsInRadius.forEach(function (d, i) {
d3.select('#n' + d).attr('fill', 'blue')
})
let total_count = hits.length
$("#hitnumber").text(total_count + " airports within 30 miles")
d3.selectAll('.list-item').remove()
let listItem = d3.selectAll('#hits').selectAll('text')
.data(hits)
.attr('padding-left', '20px')
.enter().append('div').attr('class', 'list-item')
.html(d => d.name + "<br/>Lat: " + d.latitude + "<br/>Lng: " + d.longitude + "<br/>Distance: " + d.distance)
if (hits.length == 1) {
} else {
const hitids = []
for (i = 0; i < hits.length; i++) {
hitids.push('#n' + hits[i].id)
}
let idstoget = hitids.toString()
let forceids = d3.selectAll(idstoget)
simulation.force('x', d3.forceX().strength(0.1).x(radiusPoint[0]))
simulation.force('y', d3.forceY().strength(0.1).y(radiusPoint[1]))
// simulation.force('center', d3.forceCenter(radiusPoint[0], radiusPoint[1]))
simulation.alpha(1).restart()
simulation.nodes(forceids.data())
.on('tick', ticked)
function ticked() {
update(forceids)
}
function update(forceids) {
forceids
.attr('cx', function (d) { return d.x })
.attr('cy', function (d) { return d.y })
}
}
}
function nearest(node, radius, hits) {
if (!hits) hits = [];
var r = radius,
nx1 = node.longitude - r,
nx2 = node.longitude + r,
ny1 = node.latitude - r,
ny2 = node.latitude + r;
return function (quad, x1, y1, x2, y2) {
if (quad.data && (quad.data !== node)) {
var x = node.longitude - quad.data.longitude,
y = node.latitude - quad.data.latitude,
l = Math.sqrt(x * x + y * y),
r = radius;
if (l < r) {
hits.push(quad.data)
} else {
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
}
}
.tooltip {
position: absolute;
padding: 15px;
font: 12px sans-serif;
background: #fff;
color: #000;
border: 0px;
pointer-events: none;
opacity: 0.8;
visibility: hidden;
-moz-box-shadow: 0 0 15px #aaa;
-webkit-box-shadow: 0 0 15px #aaa;
box-shadow: 0 0 15px #aaa;
}
.close {
float: right;
margin-top: 1 px;
}
.multiple-choice {
padding: 3px 0;
}
.radius {
fill-opacity: 0.15;
stroke: #333;
stroke-dasharray: 4 2;
z-index: 1000;
fill: #bff4ff;
display: none;
}
#panel {
position: absolute;
left: 1030px;
top: 0px;
width: 300px;
padding-top: 50px;
}
.list-item {
padding: 10px;
}
label {
margin: 0;
padding: 0;
font-family: Arial, Helvetica, sans-serif;
}
#hitnumber {
font-family: Arial, Helvetica, sans-serif;
padding-left: 10px;
}
.hide {
display: none;
}
<script src="https://cdn.jsdelivr.net/npm/d3-quadtree@3"></script>
<script src="https://d3js.org/d3.v6.min.js"></script>
<script src='https://npmcdn.com/@turf/turf/turf.min.js'></script>
<script src="//code.jquery.com/jquery-1.10.2.js"></script>
<script src="https://unpkg.com/topojson-client@3"></script>
<script src="https://unpkg.com/geo-albers-usa-territories@0.1.0/dist/geo-albers-usa-territories.js"></script>
<div id="content"></div>
<div id="panel">
<div id="list">
<div id="hitnumber"></div>
<pre><div id="hits"></div></pre>
</div>
</div>
<div id="chart-title"></div>