Curved lines in d3 - javascript

Doing a project in information visualization and wants to draw multiple lines from airport to airport.
Managed to get it working with great arcs, but since there is multiple flights to and from the same airport, I want to have different radiuses on the lines. Is this possible in d3?
EDIT: Here is the current code:
this.formatedflightdata = {type: "FeatureCollection", features: formatFlightData(this.flightdata)};
console.log(this.formatedflightdata);
var line = this.g.selectAll(".arc")
.data(this.formatedflightdata.features);
line.enter().append("path")
.attr("class", "arc")
.attr("fill", "none")
.attr("stroke", "red")
.attr("stroke-width", "2px")
.attr("stroke-linecap", "round")
.attr("opacity", "1")
.attr("d", this.path)
.on("click", function(d) {
console.log("Clicked line!")
});
function formatFlightData(array) {
var data = [];
array.map(function (d, i) {
var feature = {
"type": "Feature",
"geometry": {
"type": "LineString",
"coordinates": [
[d.origlong, d.origlat],
[d.destlong, d.destlat]]
},
"properties": {
"origin": d.ORIGIN,
"destination": d.DEST,
"dayOfMonth": d.DAY_OF_MONTH,
"flightDate": d.FL_DATE,
"carrier": d.CARRIER,
"distance": d.DISTANCE
}
};
data.push(feature);
});
return data;
}
Current result

Yes, it is possible. This is more of a "raw" svg question than a d3 question.
d3 is creating an svg path element. See https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths for more info.
Basically, you can construct your path to have any type of curve that you like. Specific to your example, you may ? want to insert additional elements to the array representing control points. Then, in the .attr("d", portion, construct strings with the appropriate curves.
Untested example:
attr("d", this.path[0] + ""Q 12 17 " + this.path[1])
The "Q 12 17" is trying to make a quadratic curve, with (12,17) as an arbitrarily chosen control point.

Related

Drawing circles via d3js and converting coordinates

I have following d3js code that draws country map based on geojson via d3.json and renderMap function. I want to add cities to the map that will be represented as a circles. Cities will be in the same svg as a country map.
Cities are in ua_cities_admin.csv and rendered via renderCity function.
What I don't understand is how to place data array from renderCity in the svg component and how to map city coordinates to country map.
Any help with this will be appreciated.
<!DOCTYPE html>
<meta charset="utf-8">
<head>
<title>geoPath measures</title>
</head>
<body>
<div align="center">
<svg id="my_dataviz"></svg>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.9.1/d3.min.js"></script>
<script>
var height = window.innerHeight - 100;
var width = window.innerWidth - 100;
var svg = d3.select('#my_dataviz')
.attr("width", width)
.attr("height", height);
function renderMap(data) {
var projection = d3.geoIdentity().reflectY(true).fitSize([width, height], data)
var geoGenerator = d3.geoPath().projection(projection);
svg.append("g")
.selectAll("path")
.data(data.features)
.enter()
.append('path')
.attr('d', geoGenerator)
.attr('fill', 'steelblue');
};
function renderCity(data) {
var projection = d3.geoIdentity().reflectY(true).fitSize([width, height], data)
console.log(data)
svg.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cx", 50)
.attr("cy", 50)
.attr("r", 3);
};
d3.json('https://raw.githubusercontent.com/EugeneBorshch/ukraine_geojson/master/Ukraine.json', renderMap);
d3.csv('ua_cities_admin.csv', renderCity);
</script>
</body>
</html>
ua_cities_admin.csv file has following fields:
City
lat
lng
Here's your premise:
You have two input data sets consisting of geographic data, a geojson file and a csv file. Both use latitude longitude as their coordinate system.
You want to project them, but you want them to share the same projected coordinate system, so that they align with one another.
However, if you use two different projections you won't get alignment: any common coordinate in the two input datasets will be projected differently by each different projection.
The simplest way to fix this is to reuse the same projection for both datasets, and to use a geographic projection, something like:
var projection = d3.geoMercator()
var geoGenerator = d3.geoPath().projection(projection);
function renderMap(data) {
projection.fitSize([width, height], data)
svg.append("g")
.selectAll("path")
.data(data.features)
.enter()
.append('path')
.attr('d', geoGenerator)
.attr('fill', 'steelblue');
};
function renderCity(data) {
svg.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cx", d=>projection([d.lon,d.lat])[0])
.attr("cy", d=>projection([d.lon,d.lat])[1])
.attr("r", 3);
};
d3.json("file", function(geojson) {
d3.csv("file", function(csv) {
renderMap(geojson);
renderCity(csv);
})
})
I nested your requests because otherwise, whichever file loads first will be drawn first. We also need the geojson to be loaded first to set the projection data that will be used for the csv's circles.
Additional Detail
Projection.fitSize()
For reference, projection.fitSize() requires a valid geojson object. The data generated by d3.csv is an array of objects. We need to convert this to geojson if we want fitSize to work. Geojson point features look like:
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [longitude, latitude]
},
"properties": { /* ... */ }
}
So to create geojson we need to process your data a bit:
var features = data.map(function(d) {
return {
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [d.lng, d.lat]
},
"properties": { "name":d.city }
}
})
But we do need to pass an object, not an array, so we need to create a geojson feature collection:
var featureCollection = { type:"FeatureCollection", features:features }
Projection vs Identity
Unlike d3.geoIdentity, a d3.geoProjection can be passed a coordinate to project. d3.geoIdenity can work, but with some tweaking to the above code; however, it may result in an abnormal shape or unfamiliar representation of the area of interest as it basically implements a Plate Carree projection: lat/long treated as planar with appropriate scaling/translating to center and size the map appropriately. A d3.geoProjection could be suitable here, projections also allow for fitSize() to be used.
If you wish for the same shape as d3.geoIdentity, you can use d3.geoEquirectangular() in place of d3.geoMercator in the code above.
Manually Setting Projection Parameters
fitSize() sets a identity's/projection's scale and translate - that's it.
You could define the projection once, rather than waiting to redefine its parameters once the data is loaded. If your data is static, you could extract the scale and translate values used by fitSize() by logging projection.scale() and projection.translate() after you run fitSize(), and then use those values to set the projection scale and translate yourself. This could mean you wouldn't need to wait for the geojson to load before drawing any circles (provided you ensure that you are not drawing the geojson over top of the circles).

d3 fitExtent function not mapping GeoJSON

I am trying to map the geojson linked below, but i am having trouble with the projection. When i try to pass the GeoJSON feature collection into fitExtent, no map appears (no paths are drawn).
PS;
I have used console log to break down each part of the code and everything seems fine i.e i have tested the file without the fitExtent function using regular scale and translate on the projection.
code snippet
let boroughProjection = d3.geoMercator().fitExtent(
[
[0, 0],
[width, height],
],
geojson
);
let boroughPath = d3.geoPath().projection(boroughProjection);
let boroughMap = this.mapCanvasB
.selectAll("path")
.data(geojson)
.enter()
.append("path")
.attr("class", "borough")
.attr("d", boroughPath)
.style("fill", "gray")
.style("stroke", "black")
.style("stroke-width", "0.4")
Link to GeoJSON https://raw.githubusercontent.com/blackmad/neighborhoods/master/london.geojson

d3.js build connections by using loop function

I have build a connection by using d3. The codes show the data and method of connection:
var places = {
TYO: [139.76, 35.68],
BKK: [100.48, 13.75],
BER: [13.40, 52.52],
NYC: [-74.00, 40.71],
};
var connections = {
CONN1: [places.TYO, places.BKK],
CONN2: [places.BER, places.NYC],
};
...
svg.append("path")
.datum({type: "LineString", coordinates: connections.CONN1})
.attr("class", "route")
.attr("d", path);
svg.append("path")
.datum({type: "LineString", coordinates: connections.CONN2})
.attr("class", "route")
.attr("d", path);
You can see my codes, that I use the two identical methods to build two connections. That is not good to build more connections.
I am wondering, if there is a loop function to interpret the connections by using data "connections" directly? I mean, I could get information for data "connections" and use them directly to build connections.
I have thought some ways, such as .datum({type: "LineString", function(d,i) {
return coordinates: connections[i];});. But it does not work.
Could you please tell me some way to solve it? Thanks.
Generally when you want to append many features in d3, you want to use an array not an object. With an array you can use a d3 enter selection which will then allow you to build as many features as you need (if sticking to an object, note that connections[0] is not what you are looking for, connections["conn1"] is).
Instead, use a data structure like:
var connections = [
[places.TYO, places.NYC],
[places.BKK, places.BER],
...
]
If you must have identifying or other properties for each datapoint use something like:
var connections = [
{points:[places.TYO, places.NYC],id: 1,...},
{points:[places.BKK, places.BER],id: 2,...},
...
]
For these set ups you can build your lines as follows:
paths = svg.selectAll(".connection")
.data(connections)
.enter()
.append("path")
.attr("class","connection")
.attr('d', function(d) {
return path ({
type:"LineString",
coordinates: d
});
})
See here. Or:
paths = svg.selectAll(".connection")
.data(connections)
.enter()
.append("path")
.attr("class","connection")
.attr('d', function(d) {
return path ({
type:"LineString",
coordinates: d.points
});
})
Alternatively, you can use a data set up like:
var connections = [
{target:"TYO", source:"NYC"},
{target:"BKK", source: "BER"},
...
]
paths = svg.selectAll(".connection")
.data(connections)
.enter()
.append("path")
.attr("class","connection")
.attr('d', function(d) {
return path ({
type:"LineString",
coordinates: [ places[d.source],places[d.target] ]
});
})
See here.
If selecting elements that don't yet exist, using these lines
d3.select("...")
.data(data)
.enter()
.append("path")
will append a path for each item in the data array - this means that d3 generally avoids the use of for loops as the desired behavior is baked right into d3 itself.

d3 create svg from JSON paths

I have a json file which contain multiple paths
Json file
{
"paths": {
"path1": {
"name": "PathName1",
"path": "m 411.42,469.19 -0.98 ... z",
},
"path2": {
"name": "PathName2",
"path": "m 396.03,243.46 1.48,0.11 0,0 -0.01,0.47 -0.95,1.45 ... z",
}
}
}
And I would like to create an svg from this Json file using d3, but I don't know how to use my paths
var svg = d3.select('#map').append("svg")
.attr("id", "svg")
.attr("width", 500)
.attr("height", 500);
d3.json("urlToJsonFile", function(req, data) {
// how can I show my paths ?
}
If you have several paths, and want to draw them all, you might want to parse your JSON data into a collection to use data()/enter()/exit() joins (or process them in a loop or use some other optimisation technique).
To use the data in the JSON structure you provided you can path objects, bind each "path" object as "datum", and pass the path data to its "d" attribute. This will draw one of your paths:
svg.append("path")
.attr("class", "path1")
.datum(data.paths.path1)
.attr("d", function(d) { return d.path});
Try it out: JSFiddle

How do I update my multiple line chart data in D3?

I've picked apart a couple of different examples to get as far as I have. I have a simple two-line line chart being fed by an object. For testing, I created a function that increments the data by adding a new datapoint to the end. I would like my chart to reflect those additions.
What works:
The data gets added to the object.
When button clicked, YScale adjusts
What doesn't work:
XScale doesn't adjust for new data.
Team 1 line adapts Team2 line's data and then scales along the Y accordingly.
It's a mess. And Just when I think I understand D3, the data incorporation humbles me!
Here's some code and a mostly-working fiddle.
UPDATED: Here's an updated fiddle that has it working as expected. I know D3 makes a lot of this stuff easier so, while it works, I'm looking for a more graceful way to get the same outcome.
My data format...
//SET SAMPLE DATA
var data = [{
"team": "Team 1",
"score": 0,
"elapsedTime": 0
}, {
"team": "Team 1",
"score": 2,
"elapsedTime": 3
},
...
{
"team": "Team 2",
"score": 18,
"elapsedTime": 60
}];
My update function...
function updateChart(){
//SPLIT UPDATED DATA OBJECT INTO TWO TEAMS
dataGroup = d3.nest()
.key(function(d) {
return d.team;
})
.entries(data);
//DRAW LINES
dataGroup.forEach(function(d, i) {
// Select the section we want to apply our changes to
var vis = d3.select("#visualisation").transition();
xScale.domain([0, d3.max(data, function(d) {
return d.elapsedTime;
})]);
yScale.domain([0, d3.max(data, function(d) {
return d.score;
})]);
//PROBLEM: Team 1 line changes to Team 2's data
vis.select("#line_team1")
.duration(750)
.attr("d", lineGen(d.values));
});
vis.select("#line_team2")
.duration(750)
.attr("d", lineGen(data));
// X-SCALE NEVER ADJUSTS
vis.select(".x.axis") // change the x axis
.duration(750)
.call(xAxis);
// Y-AXIS SEEMS TO SCALE AS EXPECTED
vis.select(".y.axis") // change the x axis
.duration(750)
.call(yAxis);
};
Any insight would be greatly appreciated!
One way to further simplify your code is to change it to update based on the data itself.
A method that I often use is obtaining the selector for updating the data from the data itself. In your case you could use the key property of your dataGroup objects:
dataGroup.forEach(function(d, i) {
vis.select("#line_"+d.key.toLowerCase().split(' ').join(''))
.duration(750)
.attr("d", lineGen(d.values));
var maxi = d3.max(data, function(d) {
return d.elapsedTime;
});
...
});
Or you could do the preprocessing in your d3.nest() call in order to obtain the key in form of team1 and use it as a selector.
Here's a fork of your fiddle with working example.

Categories

Resources