How to pass a value to line generation function in D3 - d3.js

Following code is used to generate lines in D3:
var lineFn = d3.line()
.x((d) => this.base.xAxis.scale(d.x))
.y((d) => this.base.yAxes[0].scale(d.y));
// series is a collection of lines I want to plot
series = [
{
data: [{x: 10, y: 20}, {x: 20, y: 30}],
yAxis: 0, // this indicates which y-axis to use
color: red
},
...
];
_.forEach(series, (line) => {
this.base.chart.append("path")
.datum(line.data)
.attr("class", "line")
.attr("d", lineFn)
.style("stroke", line.color)
});
My chart uses dual y-axes using d3.axisLeft() and d3.axisRight().
Right now, I am hardcoding the value of which y-axis to use in the lineFn.
.y((d) => this.base.yAxes[0].scale(d.y)); // 0-left axis, 1-right axis
What I would like to do is pass that value when I call the line function, something like:
.attr("d", lineFn(line.yAxis))
Is there any way to achieve this?
Thanks.

The easiest way to achieve what you want is simply creating two different line generators.
However, since you asked (not verbatim) "is it possible to define the scale dynamically when calling the line generator?", the answer is: yes, it is possible. Let's see how to do it.
In this example, I'm using an object to store the different scales:
var scales = {
yScaleLeft: d3.scaleLinear()
.domain([0, 100])
.range([170, 30]),
yScaleRight: d3.scaleLinear()
.domain([0, 200])
.range([170, 30])
};
And, in the dataset, defining which scale and color should be used for each line, just as you did:
var data = [{
data: [{
x: 1,
y: 20
}, {
...
}, {
x: 8,
y: 50
}],
yAxis: "yScaleLeft",
color: "red"
}, {
data: [{
x: 3,
y: 120
}, {
...
}, {
x: 9,
y: 180
}],
yAxis: "yScaleRight",
color: "blue"
}];
Then, when calling the line generator, we set a variable (in this case, thisScale) to specify the scale:
var thisScale;
paths.attr("stroke", d => d.color)
.attr("d", d => {
thisScale = scales[d.yAxis]
return line(d.data);
})
.attr("fill", "none");
Here is the demo, the red line uses a scale going from 0 to 100, the blue line uses a scale going from 0 to 200:
var svg = d3.select("body").append("svg")
.attr("width", 500)
.attr("height", 200);
var thisScale;
var line = d3.line()
.x(d => xScale(d.x))
.y(d => thisScale(d.y))
.curve(d3.curveMonotoneX);
var data = [{
data: [{
x: 1,
y: 20
}, {
x: 2,
y: 30
}, {
x: 3,
y: 10
}, {
x: 4,
y: 60
}, {
x: 5,
y: 70
}, {
x: 6,
y: 80
}, {
x: 7,
y: 40
}, {
x: 8,
y: 50
}],
yAxis: "yScaleLeft",
color: "red"
}, {
data: [{
x: 3,
y: 120
}, {
x: 4,
y: 130
}, {
x: 5,
y: 10
}, {
x: 6,
y: 120
}, {
x: 7,
y: 40
}, {
x: 8,
y: 130
}, {
x: 9,
y: 180
}],
yAxis: "yScaleRight",
color: "blue"
}];
var scales = {
yScaleLeft: d3.scaleLinear()
.domain([0, 100])
.range([170, 30]),
yScaleRight: d3.scaleLinear()
.domain([0, 200])
.range([170, 30])
};
var xScale = d3.scalePoint()
.domain(d3.range(11))
.range([30, 470])
var paths = svg.selectAll("foo")
.data(data)
.enter()
.append("path");
paths.attr("stroke", d => d.color)
.attr("d", d => {
thisScale = scales[d.yAxis]
return line(d.data);
})
.attr("fill", "none");
var xAxis = d3.axisBottom(xScale);
var yAxisLeft = d3.axisLeft(scales.yScaleLeft);
var yAxisRight = d3.axisRight(scales.yScaleRight);
var gX = svg.append("g").attr("transform", "translate(0,170)").call(xAxis);
var gY = svg.append("g").attr("transform", "translate(30,0)").call(yAxisLeft);
var gY2 = svg.append("g").attr("transform", "translate(470,0)").call(yAxisRight);
<script src="https://d3js.org/d3.v4.min.js"></script>
And here the same solution, but using an array (instead of an object) to store the scales, as you asked in your question:
yAxis: 0//indicates the left axis
yAxis: 1//indicates the right axis
var svg = d3.select("body").append("svg")
.attr("width", 500)
.attr("height", 200);
var thisScale;
var line = d3.line()
.x(d => xScale(d.x))
.y(d => thisScale(d.y))
.curve(d3.curveMonotoneX);
var data = [{
data: [{
x: 1,
y: 20
}, {
x: 2,
y: 30
}, {
x: 3,
y: 10
}, {
x: 4,
y: 60
}, {
x: 5,
y: 70
}, {
x: 6,
y: 80
}, {
x: 7,
y: 40
}, {
x: 8,
y: 50
}],
yAxis: 0,
color: "red"
}, {
data: [{
x: 3,
y: 120
}, {
x: 4,
y: 130
}, {
x: 5,
y: 10
}, {
x: 6,
y: 120
}, {
x: 7,
y: 40
}, {
x: 8,
y: 130
}, {
x: 9,
y: 180
}],
yAxis: 1,
color: "blue"
}];
var scales = [d3.scaleLinear()
.domain([0, 100])
.range([170, 30]), d3.scaleLinear()
.domain([0, 200])
.range([170, 30])
];
var xScale = d3.scalePoint()
.domain(d3.range(11))
.range([30, 470])
var paths = svg.selectAll("foo")
.data(data)
.enter()
.append("path");
paths.attr("stroke", d => d.color)
.attr("d", d => {
thisScale = scales[d.yAxis]
return line(d.data);
})
.attr("fill", "none");
var xAxis = d3.axisBottom(xScale);
var yAxisLeft = d3.axisLeft(scales[0]);
var yAxisRight = d3.axisRight(scales[1]);
var gX = svg.append("g").attr("transform", "translate(0,170)").call(xAxis);
var gY = svg.append("g").attr("transform", "translate(30,0)").call(yAxisLeft);
var gY2 = svg.append("g").attr("transform", "translate(470,0)").call(yAxisRight);
<script src="https://d3js.org/d3.v4.min.js"></script>

Related

How do I dynamically update the coordinates for nodes in a D3 forceSimulation?

I am trying to update the position of a node, based on a click event.
I can see that the data is updated in console, however the x,y coordinates aren't updating. I was expecting the forceSimulation to update the x,y coordinates as soon as the data changed.
Obviously, I have misunderstood how this works. But if you could state where exactly I was wrong in my intuition, that would really help me out. My thought process was that the force simulation would update the the forceX and forceY coordinates based on the foci_dict.
In console : {name: "b", age: 27, index: 0, x: 45.46420808466252, y: 54.94672336907456, …}
The object was updated, however the coordinates didn't update.
<html>
<body>
<canvas id="bubbles" width="1500" height="500"></canvas>
<button onClick="changeMe()"> clicck me </button>
</body>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
var height = 500;
var width = 1500;
var canvas = d3.select("#bubbles");
var ctx = canvas.node().getContext("2d");
var r = 5;
var data = [
{ name: "a", age: 27 },
{ name: "a", age: 21 },
{ name: "a", age: 37 },
{ name: "b", age: 23 },
{ name: "b", age: 33 },
{ name: "c", age: 23 },
{ name: "d", age: 33 }
];
var foci_points = {
'a': { x: 50, y: 50 },
'b': { x: 150, y: 150 },
'c': { x: 250, y: 250 },
'd': { x: 350, y: 350 }
};
var simulation = d3.forceSimulation()
.force("x", d3.forceX(function(d){
return foci_points[d.name].x;
}))
.force("y", d3.forceY(function(d){
return foci_points[d.name].y;
})
)
.force("collide", d3.forceCollide(r+1))
.force("charge", d3.forceManyBody().strength(10))
.on("tick", update);
simulation.nodes(data);
function update() {
ctx.clearRect(0, 0, width, height);
ctx.beginPath();
data.forEach(drawNode);
ctx.fill();
}
function changeMe(){
data[0].name="b";
console.log(data);
}
function drawNode(d) {
ctx.moveTo(d.x, d.y);
ctx.arc(d.x, d.y, r, 0, 2 * Math.PI);
}
update();
</script>
</html>
There are two issues you have to address:
The forces will not update automatically just because the underlying data has changed. You have to programmatically re-initialize the forces by calling force.initialize(nodes). Given your code this could be somewhat along the following lines:
simulation.force("x").initialize(data); // Get the x force and re-initialize.
simulation.force("y").initialize(data); // Get the y force and re-initialize.
By the time you click the button the simulation will have cooled down or even stopped. To get it going again you have to reheat it by setting alpha to some value greater than alphaMin and by restarting the simulation run.
simulation.alpha(1).restart(); // Reheat and restart the simulation.
These actions can best be done in your changeMe() function.
Have a look at the following snippet for a running demo:
var height = 500;
var width = 1500;
var canvas = d3.select("#bubbles");
var ctx = canvas.node().getContext("2d");
var r = 5;
var data = [{
name: "a",
age: 27
},
{
name: "a",
age: 21
},
{
name: "a",
age: 37
},
{
name: "b",
age: 23
},
{
name: "b",
age: 33
},
{
name: "c",
age: 23
},
{
name: "d",
age: 33
}
];
var foci_points = {
'a': {
x: 50,
y: 50
},
'b': {
x: 150,
y: 150
},
'c': {
x: 250,
y: 250
},
'd': {
x: 350,
y: 350
}
};
var simulation = d3.forceSimulation()
.force("x", d3.forceX(function(d) {
return foci_points[d.name].x;
}))
.force("y", d3.forceY(function(d) {
return foci_points[d.name].y;
}))
.force("collide", d3.forceCollide(r + 1))
.force("charge", d3.forceManyBody().strength(10))
.on("tick", update);
simulation.nodes(data);
function update() {
ctx.clearRect(0, 0, width, height);
ctx.beginPath();
data.forEach(drawNode);
ctx.fill();
}
function changeMe() {
data[0].name = "b";
simulation.force("x").initialize(data);
simulation.force("y").initialize(data);
simulation.alpha(1).restart();
}
function drawNode(d) {
ctx.moveTo(d.x, d.y);
ctx.arc(d.x, d.y, r, 0, 2 * Math.PI);
}
update();
<script src="https://d3js.org/d3.v4.min.js"></script>
<body>
<button onClick="changeMe()"> clicck me </button>
<canvas id="bubbles" width="1500" height="500"></canvas>
</body>

How to turn dataset into multiple different color lines in D3.JS

Single Line Working with following Dataset:
var dataset = [{ x: 0, y: 100 }, { x: 1, y: 833 }, { x: 2, y: 1312 },
{ x: 3, y: 1222 }, { x: 4, y: 1611 },]
]
Multi Line dataset (Which produces no line)
var dataset = [{ x: 0, y: 100 }, { x: 1, y: 833 }, { x: 2, y: 1312 },
{ x: 3, y: 1222 }, { x: 4, y: 1611 },
{ x: 0, y: 200 }, { x: 1, y: 933 }, { x: 2, y: 1412 },
{ x: 3, y: 1322 }, { x: 4, y: 1711 },]
This is the D3.JS Code, which works for one line but doesn't produce a second line. What would be the best modification to make to this code so I can pass in the multiline dataset with up to 10 series of data. Also what is the best way to have each line be a different color?
var margin = { top: 20, right: 100, bottom: 30, left: 100 },
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var xScale = d3.scale.linear()
.domain([0, d3.max(dataset, function (d) { return d.x; })])
.range([0, width]);
var yScale = d3.scale.linear()
.domain([0, d3.max(dataset, function (d) { return d.y; })])
.range([height, 0]);
var xAxis = d3.svg.axis()
.scale(xScale)
.ticks(0)
.orient("bottom")
.outerTickSize(0)
.tickPadding(0);
var yAxis = d3.svg.axis()
.scale(yScale)
.orient("left")
.outerTickSize(0)
.tickPadding(10);
var line = d3.svg.line()
.x(function (d) { return xScale(d.x); })
.y(function (d) { return yScale(d.y); });
//Create SVG element
var svg = d3.select("#visualisation")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
svg.append("path")
.data([dataset])
.attr("class", "line0")
.attr("d", line);
First dataset, which is an array of points, needs to become an array of arrays of points:
var dataset = [
[{ x: 0, y: 100 }, { x: 1, y: 833 }, { x: 2, y: 1312 }, { x: 3, y: 1222 }, { x: 4, y: 1611 }],
[{ x: 0, y: 200 }, { x: 1, y: 933 }, { x: 2, y: 1412 }, { x: 3, y: 1322 }, { x: 4, y: 1711 }]
]
Subsequently, your x and y max calculations for the x and y domains need to find the max across all arrays of points. So they have to gain a level of nested-ness like this:
var xScale = d3.scale.linear()
.domain([0, d3.max(dataset, function(series) {
return d3.max(series, function (d) { return d.x; })
})])
.range([0, width]);
(Same idea for yScale)
Finally, now that there are multiple lines, you should use d3.selection with .enter() to create one <path> per series, like so:
svg.selectAll("path.line").data(dataset).enter()
.append("path")
.attr("class", "line")
.attr("d", line);
Here's a working jsFiddle

How to add legend and make lines random color as a new line is added to D3.JS

Every time I add to the below dataset a new line is addedd to my chart. What changes do I need to make in order for this to be a different random color each time, and how/where should I add a legend for thes random lines?
Working Fiddle https://jsfiddle.net/qvrL3ey5/
var dataset = [
[{ x: 0, y: 100 }, { x: 1, y: 833 }, { x: 2, y: 1312 }, { x: 3, y: 1222 }, { x: 4, y: 1611 }],
[{ x: 0, y: 200 }, { x: 1, y: 933 }, { x: 2, y: 1412 }, { x: 3, y: 1322 }, { x: 4, y: 1711 }]]
Code:
var dataset = [
[{ x: 0, y: 100 }, { x: 1, y: 833 }, { x: 2, y: 1312 }, { x: 3, y: 1222 }, { x: 4, y: 1611 }],
[{ x: 0, y: 200 }, { x: 1, y: 933 }, { x: 2, y: 1412 }, { x: 3, y: 1322 }, { x: 4, y: 1711 }]]
var margin = { top: 20, right: 100, bottom: 30, left: 100 },
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var xScale = d3.scale.linear()
.domain([0, d3.max(dataset, function(series) {
return d3.max(series, function (d) { return d.x; })
})])
.range([0, width]);
var yScale = d3.scale.linear()
.domain([0, d3.max(dataset, function(series) {
return d3.max(series, function (d) { return d.y; })
})])
.range([height, 0]);
var xAxis = d3.svg.axis()
.scale(xScale)
.ticks(0)
.orient("bottom")
.outerTickSize(0)
.tickPadding(0);
var yAxis = d3.svg.axis()
.scale(yScale)
.orient("left")
.outerTickSize(0)
.tickPadding(10);
var line = d3.svg.line()
.x(function (d) { console.log(d.x, xScale(d.x));return xScale(d.x); })
.y(function (d) { return yScale(d.y); });
//Create SVG element
var svg = d3.select("#visualisation")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
svg.selectAll("path.line").data(dataset).enter()
.append("path")
.attr("class", "line")
.attr("d", line);
Edit:
Found how to add the color..
var color = d3.scale.category10();
.attr("d", line)
.style("stroke", function (d) {
return color(d);
Still researching how to add a legend.
Edit:
Unfortunately the color's are the same, unless the data-sets are different lengths.. If I have data-set A) 25 points and B) 30 points it generates two different colors, but if both A&B are 25 points then its the same color.. Any ideas?
Regarding the colours: var color = d3.scale.category10() will create a ordinal scale with empty domain and range with ten colors, but if no domain is set the domain will be automatically created according to the sequence you call color. So, we have to set a domain:
var color = d3.scale.category10();
color.domain(d3.range(0,10));
And then painting the lines randomly:
.style("stroke", function (d) {
return color(Math.floor(Math.random()*10))});
This satisfies your request for the lines:
to be a different random color each time
Check this fiddle, every time you click "run" the colors are different: https://jsfiddle.net/gerardofurtado/qvrL3ey5/1/
Regarding the legend, you'll have to give us more information.

Flip the x-axis of a D3 Stacked Bar Chart

Chapter 11 of "Interactive Data Visualization for the Web" shows how to create stacked bar charts with the D3.js library. The example produces an upside down chart with the bars attached to the top of the x-axis.
Flipping the chart and attaching them to the bottom is left as an exercise for the reader.
Given this starting point:
<script src="http://d3js.org/d3.v3.min.js"></script>
<script type="text/javascript">
var w = 500;
var h = 300;
var dataset = [[ { x: 0, y: 5 }, { x: 1, y: 4 }, { x: 2, y: 2 }, { x: 3, y: 7 }, { x: 4, y: 23 }],
[ { x: 0, y: 10 }, { x: 1, y: 12 }, { x: 2, y: 19 }, { x: 3, y: 23 }, { x: 4, y: 17 } ],
[ { x: 0, y: 22 }, { x: 1, y: 28 }, { x: 2, y: 32 }, { x: 3, y: 35 }, { x: 4, y: 43 } ]];
var stack = d3.layout.stack();
stack(dataset);
var xScale = d3.scale.ordinal()
.domain(d3.range(dataset[0].length)).rangeRoundBands([0, w], 0.05);
var yScale = d3.scale.linear()
.domain([0, d3.max(dataset, function(d) {
return d3.max(d, function(d) { return d.y0 + d.y; });
})])
.range([0, h]);
var colors = d3.scale.category10();
var svg = d3.select("body").append("svg").attr("width", w).attr("height", h);
var groups = svg.selectAll("g").data(dataset).enter().append("g")
.style("fill", function(d, i) { return colors(i); });
var rects = groups.selectAll("rect")
.data(function(d) { return d; })
.enter()
.append("rect")
.attr("x", function(d, i) { return xScale(i); })
.attr("y", function(d) { return yScale(d.y0); })
.attr("height", function(d) { return yScale(d.y); })
.attr("width", xScale.rangeBand());
</script>
What needs to be done to flip the chart?
The solution I came up with involves three changes:
Change the yScale .range from:
.range([0, h]);
to:
.range([h, 0]);
Change the rect "y" .attr from:
.attr("y", function(d) { return yScale(d.y0); })
to:
.attr("y", function(d) { return yScale(d.y0) + yScale(d.y) - h; })
Change the rect "height" .attr from:
.attr("height", function(d) { return yScale(d.y); })
to:
.attr("height", function(d) { return h - yScale(d.y); })
With those changes applied, the stacks attach to the bottom and still maintain their relative sizes.

d3.time scale returning NaN

I have the following example
http://jsfiddle.net/BSB42/
and I can't seem to figure out why the x scale is not working for me (Getting NaN for my cx values). Here is an excerpt of my code:
var parse = d3.time.format("%Y").parse;
var data = [
{ year: "2008", number : 3},
{ year: "2009", number : 10},
{ year: "2010", number : 17},
{ year: "2011", number : 23},
{ year: "2012", number : 34},
{ year: "2013", number : 50}
];
for (var i = 0; i < data.length; i++) {
var d = data[i];
d.year = parse(d.year);
};
var yearScale = d3.time.scale()
.domain(d3.extent(data, function (d) { return d.year;}))
.range(50, window.innerWidth - 50);
var numberScale = d3.scale.linear()
.domain([0, d3.max(data, function (d) { return d.number;})])
.range([50, window.innerHeight - 50]);
svg.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr({
cx: function (d) { return yearScale(d.year); },
cy: function (d) { return window.innerHeight - numberScale(d.number)},
r: 4,
fill: "#fff",
stroke: "#78B446",
"stroke-width": 4
})
.range() needs an array so change to .range([50, window.innerWidth - 50]); in your yearScale. You did it correctly for the numberScale already.
updated jsFiddle

Resources