I have a map that has some overlapping points. I'm using a quadtree and turf.js to determine, when I click on a point, how many other points are within a 30 mile radius.
What I'd like to do (if there's more than one point in that radius) is use d3.forceSimulation to equally spread out the overlapping points.
This is a pretty close example of what I want to do, but uses d3v3 and google maps:
Help very much appreciated!!
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/[email protected]/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/[email protected]/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>
<iframe name="sif1" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>
CodePudding user response:
While I'd be tempted to not use a force layout for this, I'll work with the code you have here (though the question of the lines connecting the circles to their original location is not addressed here) and quickly address why the circles do not behave as you expect.
A force layout will create the appropriate properties on a node if they don't exist. For position of a node, these properties are d.x and d.y. Your data does not have x or y properties, so when you create the force, the nodes are initialized with values around the origin, [0,0], which is why they migrate to the top left corner. This problem can be solved by creating x and y properties:
.each(d=>[d.x,d.y] = projection([d.longitude, d.latitude]))
In the snippet below I use this right after positioning the circles on enter.
Secondly, you want to pass the bound datums to the force layout not the nodes (otherwise, as the nodes themselves don't have x,y properties, we'll initialize them in the top left again). We also don't want to pass the selection as the nodes, instead, let's access the selection's data:
simulation.nodes(forceids.data())
Which, with a quick and dirty few lines to reset the moved nodes (using the lat/long, and resetting x,y to the lat/long), gives us:
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/[email protected]/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/[email protected]/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>
<iframe name="sif2" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>