Create a line passing through some points with d3.js - javascript

I have a situation very similar to the one in this JSFiddle with some points representing a team (in particular its final rank in a football season).
I would like to substitute the points with a line passing exactly in these points so that the final result shows the temporal evolution of each team in terms of final ranking position.
I know how to create a line by setting the X1,X2,Y1,Y2 coordinates but I don't understand how to set this coordinates to the exact value (e.g. if the line is between season 2006-2007 and season 2007-2008 I will have to set X1 and Y1 with value from the first season as d[0] and d[1] but for X2 and Y2 I need values from the next element in the array.
I'm very new with D3.js so any advice and solution is very welcome. Thanks

Assuming you have already declared some data for your lines drawing the actual lines based on that data is as simple as this:
create the X and Y scales:
var xScale = d3.scale.linear().domain([dataRange.x1, dataRange.x2]).range([plotRange.x1, plotRange.x2]);
var yScale = d3.scale.linear().domain([dataRange.y1, dataRange.y2]).range([plotRange.y1, plotRange.y2]);
declare the line function:
var valueLine = d3.svg.line()
.x(function (dataItem, arrayIndex) {
return xScale(dataItem);
})
.y(function (dataItem, arrayIndex) {
return yScale(dataItem)
});
and finally create the path:
g.append("path")
.style("stroke", someColour)
.attr("d", valueLine(myData))
.attr("class", "someClass");
Refer to more documentation here: https://www.dashingd3js.com/

Based on that fiddle, this is what I'd do:
First, I'd set a class to each team's circles (team1, team2 and so on...). So, I could later retrieve the circles' values for each team.
For retrieving the circles values, I'd use a for loop:
for(var j = 1; j < 4; j++){//this loops from "Team1" to "Team3"
var team = d3.selectAll("circle.Team" + j)[0];//selects the team by class
for(var i = 0; i < team.length; i++){//this loops through the circles
if(team[i+1]){//if the next circle exists
svg.append("line")
.attr("x1", d3.select(team[i]).attr("cx"))//this circle
.attr("y1", d3.select(team[i]).attr("cy"))//this circle
.attr("x2", d3.select(team[i+1]).attr("cx"))//the next circle
.attr("y2", d3.select(team[i+1]).attr("cy"))//the next circle
.attr("stroke", function(){
return _TEAM_COLORS_["Team" + j]
});//sets the colours based on your object
}
}
};
Here is that fiddle, updated: https://jsfiddle.net/gerardofurtado/6cc0ehz2/18/

Related

Offset Line stroke-weight d3.js

I'm using d3.js to plot a highway network over a map SVG. I'd like to be able to vary the stroke-weight of the line to illustrate demand based on a value.
Highway links are define as one way, so for example a two way road would have two overlapping line elements (with separate id's). I can use stroke-weight to edit the thickness of the line based on a variable (as below), but on a two way road, the larger of the two stroke weights will always cover the smaller rendering it invisible.
Is there an easy way to offset a line by half its stroke-weight to the left hand side of the direction the line is drawn? (direction denoted by x1,y1 x2,y2)
d3.csv("links.csv", function (error, data) {
d3.select("#lines").selectAll("line")
.data(data)
.enter()
.append("line")
.each(function (d) {
d.p1 = projection([d.lng1, d.lat1]);
d.p2 = projection([d.lng2, d.lat2]);
})
.attr("x1", function (d) { return d.p1[0]; })
.attr("y1", function (d) { return d.p1[1]; })
.attr("x2", function (d) { return d.p2[0]; })
.attr("y2", function (d) { return d.p2[1]; })
.on('mouseover', tip_link.show)
.on('mouseout', tip_link.hide)
.style("stroke", "black")
.style("stroke-width", lineweight)
});
One option would be to just create new start/end points when drawing your lines and use those:
var offset = function(start,destination,distance) {
// find angle of line
var dx = destination[0] - start[0];
var dy = destination[1] - start[1];
var angle = Math.atan2(dy,dx);
// offset them:
var newStart = [
start[0] + Math.sin(angle-Math.PI)*distance,
start[1] + Math.cos(angle)*distance
];
var newDestination = [
destination[0] + Math.sin(angle-Math.PI)*distance,
destination[1] + Math.cos(angle)*distance
];
// return the new start/end points
return [newStart,newDestination]
}
This function takes two points and offsets them by a particular amount given the angle between the two points. Negative values shift to the other side, swapping the start and destination points will shift to the other side.
In action, this looks like, with the original line in black:
var offset = function(start,destination,distance) {
// find angle of line
var dx = destination[0] - start[0];
var dy = destination[1] - start[1];
var angle = Math.atan2(dy,dx);
// offset them:
var newStart = [
start[0] + Math.sin(angle-Math.PI)*distance,
start[1] + Math.cos(angle)*distance
];
var newDestination = [
destination[0] + Math.sin(angle-Math.PI)*distance,
destination[1] + Math.cos(angle)*distance
];
// return the new start/end points
return [newStart,newDestination]
}
var line = [
[10,10],
[200,100]
];
var svg = d3.select("svg");
// To avoid repetition:
function draw(selection) {
selection.attr("x1",function(d) { return d[0][0]; })
.attr("x2",function(d) { return d[1][0]; })
.attr("y1",function(d) { return d[0][1]; })
.attr("y2",function(d) { return d[1][1]; })
}
svg.append("line")
.datum(line)
.call(draw)
.attr("stroke","black")
.attr("stroke-width",1)
svg.append("line")
.datum(offset(...line,6))
.call(draw)
.attr("stroke","orange")
.attr("stroke-width",10)
svg.append("line")
.datum(offset(...line,-4))
.call(draw)
.attr("stroke","steelblue")
.attr("stroke-width",5)
<svg width="500" height="300"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
You will need to adapt this to your data structure, and it requires twice as many lines as before, because you aren't using stroke width, your using lines. This is advantageous if you wanted to use canvas.

D3: Select a circle by x and y coordinates in a scatter plot

Is there any possibility in d3.js to select the elements by their position, i.e. by their x and y coordinates? I have a scatter plot which contains a large amount of data. And i have also an array of coordinates. the dots with these coordinates should be red. I am doing something like this for that:
bestHistory() {
var that = this;
var best = d3.select("circle")
.attr("cx", that.runData[0].best_history[0].scheduling_quality)
.attr("cy", that.runData[0].best_history[0].staffing_cost)
.classed("highlighted", true)
}
This method should set the class attribute of the circles on this certain positions equal to highlighted.
And then the appropriate CSS:
circle.highlighted {
fill: red;
}
But instead getting red this dot just disappears.
How can I achieve that what I want to ?
You can calculate the actual distance of each point to the point of interest and determine points color based on this distance like:
var threshold=...
var p =...
d3.select('circle').each(function(d){
var x = p.x - d.x;
var y = p.y - d.y;
d.distance = Math.sqrt(x*x + y*y);
}).attr('fill', function(d){
return d.distance < threshold? 'red' : 'blue'
})
Ps. Sorry, answered from mobile

D3js - Getting max value from d3.line() for a specific domain

I am making a multi-line chart and I have implemented a brush to be able to zoom into a specific domain on the x-axis. However, when I zoom in I want the y-axis to scale along so that its domain goes from [0, maxY], where maxY is the maximum y-value for the current selection on the x-axis. To generate the lines I am using d3.line() (which has the connection between the x and y values). This is how I currently calculate the maxY value:
//Find max and min values in data to set the domain of the y-axis
var maxArray = updatedData.map(function(variable){
//First map the values in the array to a new array
var valuesArray = variable.values.map(function(d){
return d.value;
})
//Find max value in array
return Math.max(...valuesArray);
});
var maxY = Math.max(...maxArray);
And here is where I set the scales and create the d3.line():
var xScale = d3.scaleTime()
.range([0, chartWidth]);
var yScale = d3.scaleLinear()
.domain([0, maxY])
.range([chartHeight, 0]);
var brush = d3.brushX()
.on("end", brushend);
var line = d3.line()
.curve(d3.curveBasis)
.x(function(d) {return xScale(d.date)})
.y(function(d) {return yScale(d.value)})
//Save this to be able to zoom back out
var originalDomain = [new Date(data[0].Timestamp), new Date(data[data.length-1].Timestamp)];
xScale.domain(originalDomain);
Here is the code where I set the new xScale.domain() and zoom in on that interval (which is called when the brushing is ended):
function brushend(){
//sourceEvent - the underlying input event, such as mousemove or touchmove.
if (d3.event.sourceEvent && d3.event.sourceEvent.type === "brush") return; // ignore zoom-by-brush
var brushInterval = d3.event.selection; //The interval of the current brushed selection
//If the function is called with no selection: ignore
if(!brushInterval) return;
//Enable reset button
resetButton.attr("disabled", null)
.on("click", resetAxis);
var newDomain = brushInterval.map(xScale.invert, xScale);
//TODO: Find max and min values in data to set the domain of the y-axis
xScale.domain(newDomain);
chart.selectAll(".line")
.transition()
.duration(1000)
.attr("d", function(d){ return line(d.values)});
chart.select(".x-axis")
.transition()
.duration(1000)
.call(xAxis);
//Remove the visual brush
d3.select(".brush").call(brush.move, null);
}
What I would like to do is to find the maximum y-value in the currently selected domain. I know that I can filter the data values to remove the ones that are not in the currently selected domain and then calculate the maximum value from them (like I did for the original domain). But it seems like there should be an easier solution to this. I didn't find any function in the documentation for d3.line() that could calculate max values.
Is there any easy way to calculate max value from d3.line()?
Thanks
There is not really an easier solution to this as you somehow have to filter the values to only take into account the ones which are in your selected x domain. However, using two nested calls to d3.max() you can at least give it a pleasant look and spare some iterations by avoiding an additional call to .filter(). Since d3.max() will ignore null values you can use it to filter your values by returning null if the current datum is outside of the x domain's boundaries. To get the maximum value you can use something like the following:
const maxY = xDomain => d3.max(updatedData, variable =>
d3.max(
variable.values,
v => v.Timestamp >= xDomain[0] && v.Timestamp <= xDomain[1] ? v.value : null
)
);
Have a look at the following snippet for a working demo:
var updatedData = [{
values: [{Timestamp:0, value:1},{Timestamp:1, value:5},{Timestamp:2, value:10},{Timestamp:3, value:3},{Timestamp:4, value:30}]
}, {
values: [{Timestamp:0, value:19},{Timestamp:1, value:12},{Timestamp:2, value:13},{Timestamp:3, value:8},{Timestamp:4, value:50}]
}];
const maxY = xDomain => d3.max(updatedData, variable =>
d3.max(
variable.values,
v => (!xDomain || v.Timestamp >= xDomain[0] && v.Timestamp <= xDomain[1]) ? v.value : null
)
);
console.log(maxY()); // Default, check all values: max 50
console.log(maxY([1,3])); // Max 13
console.log(maxY([0,3])); // Max 19
<script src="https://d3js.org/d3.v4.js"></script>

D3: Animate circle along border of country on spinning globe

My problem is simple to explain but I am having real trouble implementing a solution. I am trying to animate a circle along a path on a D3 map. The twist here is that I would like to use one of Mike Bostock's spinny globes (i.e. 3D map).
In time, I would like to add other paths to the globe and to use these for my animations. For now, I would simply like to animate the circles along the border of Russia (i.e. along the path of the Russia polygon coordinates)
I have built a jsfiddle to get traction on this and you can see all my code. Unfortunately I cannot get it to work, and am hoping you can help me out. My jsfiddle: http://jsfiddle.net/Guill84/xqmevpjg/7/
I think my key difficulty is (a) actually referencing the Russia path, and I think I am not getting it right at the moment, and (b) making sure that the interpolation is calculated properly (i.e. that the animation is dynamically linked to the globe, and not just 'layered on top'). The code that is supposed to do that is as follows:
setTimeout(function(){
var path = d3.select("path#Russia"),
startPoint = pathStartPoint(path);
marker.attr("r", 7)
.attr("transform", "translate(" + startPoint + ")");
transition();
//Get path start point for placing marker
function pathStartPoint(path) {
var d = path.attr("d"),
dsplitted = d.split(" ");
return dsplitted[1].split(",");
}
function transition() {
marker.transition()
.duration(7500)
.attrTween("transform", translateAlong(path.node()))
.each("end", transition);// infinite loop
}
function translateAlong(path) {
var l = path.getTotalLength();
return function(i) {
return function(t) {
var p = path.getPointAtLength(t * l);
return "translate(" + p.x + "," + p.y + ")";//Move marker
}
}
}
I'd be hugely grateful for any help.
For the first part of your question, one way to select the path is to add an id to id :
d3.json("http://mbostock.github.io/d3/talk/20111018/world-countries.json", function(collection) {
feature = svg.selectAll("path")
.data(collection.features)
.enter().append("svg:path")
.attr("d", clip)
.attr("id", function(d) { return d.properties.name; }) ;
and then select the path like that :
var path = d3.select("#Russia").node()
Then you can select the first point with :
path.getPointAtLength(0)
See this updated fiddle : http://jsfiddle.net/xqmevpjg/11/

Is it possible to add chords or curved lines to an arc visualization with d3.js? (Without using chord.groups)

I have created an arc visualization that is a semi-circle with about 74 path elements on it, each representing a country and it's "arcValue" (expressed as the height of each path):
http://jsfiddle.net/developerAndADj/2DYrQ/
I'm looking to add chords to the visualization for countries that have a productCount > 0. Each chord would go from each it's corresponding path element to a path that sits below the arc.
I am trying to accomplish two things:
Show that certain countries have a product count and others do not.
Have curved lines or chords that represent these product counts.
So far, I have been able to use an additional arc inside of the one in the above fiddle that goes from the center of the semi-circle to each path element, however the curved effect is not there.
I have also managed to create chords based off of the product count for each country, however, I am unable to get the chords to line up with it's corresponding country. Here is my code to generate the cords:
// Initialize the square matrix
var cDataMatrix = [];
for(var h = 0; h < data.length; h++){
cDataMatrix[h] = [];
for(var k = 0; k < data.length; k++){
cDataMatrix[h][k] = 0;
}
}
// Fill the matrix with product count values
data.forEach(function(d){
for(var a = 0; a < productVals.length; a++){
if(d.productCount){
cDataMatrix[d.id][d.id] = d.productCount;
}
}
});
// Generate the chord layout
var chord = d3.layout.chord()
.padding(.1)
.matrix(cDataMatrix);
var ch = d3.svg.chord()
.radius(300)
.startAngle(function(d, i){
return arcScale(i*(Math.PI/179.2));
})
.endAngle(function(d, i){
var degree = i+0.8;
return arcScale(degree*(Math.PI/179.2));
});
// Draw the chords on the arc
var chords = indArcBody.append("g")
.attr("class", "chords");
chords.selectAll("path.chord")
.data(chord.chords)
.enter().append("svg:path")
.attr("class", "chord")
.style("fill", "#000")
.attr("d", ch);
Here is a demo of it:
http://jsfiddle.net/developerAndADj/68WRT/
Is it possible to line up each chord to it's respective path element? Also, is it possible to use chords or curved lines that go from the inner radius of each path element to a path that sits below the arc, or any specified coordinate in the SVG?
Thanks for any feedback!

Categories