I am a total beginner to d3.js so please be kind :)
considering this jsbin example
I have the following dataset:
var dataset = [
[d3.time.hour.utc.offset(now, -5), 1, 10],
[d3.time.hour.utc.offset(now, -4), 2, 20],
[d3.time.hour.utc.offset(now, -3), 3, 30],
[d3.time.hour.utc.offset(now, -2), 4, 40],
[d3.time.hour.utc.offset(now, -1), 5, 50],
[now, 6, 60],
];
Two questions.
Does d3 provide a better approach to finding the max value for my y-axis data (all columns but the 0th, the 0th column is x-axis (time)) in my dataset array? Currently I am just looping through the entire dataset array and making a second array, excluding the first column. Perhaps there is a better datastructure other than an array I should be using for this entirely?
var data_arr = [];
for (row in dataset){
for (col=1;col < dataset[row].length; col++){
data_arr.push(dataset[row][col]);
}
}
var yScale = d3.scale.linear()
.domain([0, d3.max(data_arr)])
.range([h - padding, padding]);
Once thats resolved, I still need to determine how to graph multiple y-axis values in general! This worked fine before I needed multiple y-axis values:
svg.selectAll("circle")
.data(dataset)
.enter()
.append("circle")
.attr("cx", function(d) {
return xScale(d[0]);
})
.attr("cy", function(d) {
return yScale(d[1]);
})
.attr("r", 2);
Please take a look at the graph w/ code here now for full context: http://jsbin.com/edatol/1/edit
Any help is appreciated!
I've made a couple of changes to your example and you can see the results at http://jsbin.com/edatol/2/edit.
First, I modified your data a little bit. This is mostly just a style thing, but I find it's easier to work with objects instead of arrays:
//Static dataset
var dataset = [
{x: d3.time.hour.utc.offset(now, -5), y1: 1, y2: 10},
{x: d3.time.hour.utc.offset(now, -4), y1: 2, y2: 20},
{x: d3.time.hour.utc.offset(now, -3), y1: 3, y2: 30},
{x: d3.time.hour.utc.offset(now, -2), y1: 4, y2: 40},
{x: d3.time.hour.utc.offset(now, -1), y1: 5, y2: 50},
{x: now, y1: 6, y2: 60},
];
Then you can find your domains and ranges like this:
var xDomain = d3.extent(dataset, function(i) { return i.x; });
var maxY = d3.max(dataset, function(i) { return Math.max(i.y1, i.y2); });
Then to add multiple y-values, you just have to append an additional circle with the appropriate values. I gave them different classes so that you can use that to select them if you want to do transitions or updates later on.
//Create circles
svg.selectAll(".y1")
.data(dataset)
.enter()
.append("circle")
.attr("cx", function(d) { return xScale(d.x); })
.attr("cy", function(d) { return yScale(d.y1); })
.attr("class", "y1")
.attr("r", 2);
//Create circles
svg.selectAll(".y2")
.data(dataset)
.enter()
.append("circle")
.attr("cx", function(d) { return xScale(d.x); })
.attr("cy", function(d) { return yScale(d.y2); })
.attr("class", "y2")
.attr("r", 2);
Related
Below is the code i use to draw axis and circle. I need to add rectangle based on two date and placed it under the circle. Appreciate your help. Thanks.
let scale: any = d3select.scaleTime()
//.domain([d3select.min(data), d3select.max(data)])
.domain([new Date(2020, 0, 1), new Date(2021, 0, 1)])
.range([20, axes_width - 800]);
let x_axis: any = d3select.axisBottom(scale)
.scale(scale);
axes_svg.append("g")
.call(x_axis);
var myData = [new Date(2020, 3, 1), new Date(2020, 6, 1)];
axes_svg
.selectAll('circle')
.data(myData)
.enter()
.append('circle')
.style("fill", "#69b3a2")
.attr('r', 6)
.attr('cy', 40)
.attr('cx', function (d) {
return scale(d);
});
I think your problem here is that you pass all dates in a one dimensional array. Instead of having
var myData = [new Date(2020, 3, 1), new Date(2020, 6, 1)];
like you do for the circles, you should have something like this:
var myRecData = [ {"startDate": new Date(2020, 3, 1), "endDate": new Date(2020, 6, 1)}, {"startDate": new Date(2020, 4, 1), "endDate": ....];
And then access it with d.startDate and d.endDate
EDIT:
Based on your last comment I recreated the whole code (for future questions please add a complete reproducible code, so people who want to help you can test it fast...):
<body>
<div id="mainDiv"></div>
<script>
const width = 500
const height = 500
const axes_svg = d3.select("#mainDiv").append("svg")
.attr("viewBox", [0,0, width, height])
.attr("width", "100%")
.attr("height", "100%")
let scale = d3.scaleTime()
//.domain([d3select.min(data), d3select.max(data)])
.domain([new Date(2020, 0, 1), new Date(2021, 0, 1)])
.range([20, width - 200]);
let x_axis = d3.axisBottom(scale)
.scale(scale);
axes_svg.append("g")
.call(x_axis);
var myData = [new Date(2020, 3, 1), new Date(2020, 6, 1)];
axes_svg
.selectAll('circle')
.data(myData)
.enter()
.append('circle')
.style("fill", "#69b3a2")
.attr('r', 6)
.attr('cy', 40)
.attr('cx', function (d) {
return scale(d);
});
var testdata = [{ start: new Date(2020, 1, 1), close: new Date(2020, 3, 1) }];
axes_svg
.data(testdata)
.append("rect")
.style("fill", "#00abff")
.attr("x", function (d) { return scale(d.start); })
.attr("y", 80) //distance from axis
.attr("width", function (d) { return scale(d.close) - scale(d.start); })
.attr("height", 30); //height
</script>
</body>
I'm currently stuck at a problem with d3.js regarding the positioning.
This is the code I have right now
var center = {name: "sun", count: 20 }; //Will have more complex data in the future
var planets = [
{name: "Mercury", count: 2},
{name: "Venus", count: 3} ,
{name: "Earth", count: 5},
{name: "Mars", count: 4},
{name: "Jupiter", count: 11},
{name: "Saturn", count: 10},
{name: "Uranus", count: 7},
{name: "Neptune", count: 8} ];
var svg = d3.select("#planet-chart").append("svg")
.attr("width", 800)
.attr("height", 800);
var circleContainer = svg.selectAll("g mySolarText")
.data(planets);
var circleContainerEnter = circleContainer.enter()
.append("g")
.attr("transform", function(d,i){
return "translate("+ i*10 +",80)"
});
var circle = circleContainerEnter.append("circle")
.attr("r", function(d){return d.count * 5} )
.attr("cx", function(d,i){return (i+1) * 30} )
.attr("cy", function(d,i){return (i+1) * 30} )
.attr("stroke","black")
.attr("fill", "white");
circleContainerEnter.append("text")
.attr("dx", function(d){return -20})
.text(function(d){
return d.name}
);
Issues I am having right now are:
Currently, I can only pass the planets variable into the circle container, but I wish to also include the sun variable into it but I do not know how. I cannot simply include the sun variable into the planets array variable because I will need to put more data into it in the future.
I am having a hard time trying to figure out how to radially position the
planets around the sun, and add connecting lines to them which I am hoping to do. I have tried looking into arc but I am stuck.
For now, the sizes of the planets are being adjusted with their count values which I just multiply with the radius. Forgive me I am just a student and wish to learn more about d3 js. If you guys can help me, I would be so much grateful. Or if you can lead me to references I would be so much indebted.
Thank you so much in advance.
#tomshanley helped me via d3 official help forums. he posted this code for the fix to the issues I was encountering.
http://blockbuilder.org/tomshanley/840d381a9ca87ab404f63adda1ba8452
const radians = 0.0174532925
var center = {name: "sun", count: 20 };
var planets = [
{name: "Mercury", count: 2},
{name: "Venus", count: 3} ,
{name: "Earth", count: 5},
{name: "Mars", count: 4},
{name: "Jupiter", count: 11},
{name: "Saturn", count: 10},
{name: "Uranus", count: 7},
{name: "Neptune", count: 8} ];
var w = 800, h = 800;
var svg = d3.select("body").append("svg")
.attr("width", w)
.attr("height", h);
let orbitRadius = d3.scaleLinear()
.domain([0,8]) //number of planets
.range([90,w/2]) //you may need adjust this later
let angle = d3.scaleLinear()
.domain([0,9]) //number of planets + 1
.range([0,360]) //you may need adjust this later
svg.selectAll("line")
.data(planets)
.enter()
.append("line")
.attr("x1", function(d,i){
let a = angle(i);
let r = orbitRadius(i);
return xCoord = x(a, r) + (w/2)
})
.attr("y1", function(d,i){
let a = angle(i);
let r = orbitRadius(i);
return yCoord = y(a, r) + (h/2)
})
.attr("x2", (w/2))
.attr("y2", (h/2))
.style("stroke", "black")
.style("stroke-width", 2)
var circleContainer = svg.selectAll("g mySolarText")
.data(planets);
var circleContainerEnter = circleContainer.enter()
.append("g")
.attr("transform", function(d,i){
let a = angle(i);
let r = orbitRadius(i);
let xCoord = x(a, r) + (w/2)
let yCoord = y(a, r) + (h/2)
return "translate("+ xCoord +"," + yCoord + ")"
});
var circle = circleContainerEnter.append("circle")
.attr("r", function(d){return d.count * 5} )
.attr("cx", 0)
.attr("cy", 0)
.attr("stroke","black")
.attr("fill", "white");
circleContainerEnter.append("text")
.attr("dx", function(d){return -20})
.text(function(d){
return d.name}
);
function x (angle, radius) {
// change to clockwise
let a = 360 - angle
// start from 12 o'clock
return radius * Math.sin(a * radians)
}
function y (angle, radius) {
// change to clockwise
let a = 360 - angle
// start from 12 o'clock
return radius * Math.cos(a * radians)
}
again, thanks so much! :)
Say I have a path I created with d3 something like:
line = d3.line()
.curve(d3.curveLinear)
.x(function(d) { return x(d.x);})
.y(function(d) { return y(d.y); });
data = [{x: 0, y: 0}, {x: 5, y: 5}, {x:10, y:10}];
myLine = svg.append("path")
.attr("fill", "none")
.attr("stroke", "steelblue")
.datum(data)
.attr("d", line);
This makes a nice diagonal line from 0 to 10. Now if I update my data to make some changes and add some points:
data = [{x: 1, y: 1}, {x:2, y:3}, {x: 6, y: 7}, {x:9, y:9}];
And update my line
myLine.datum(data).transition().duration(1000).attr("d", line);
It gives a weird transition where it slides the existing path to fit the first three points of the new path and awkwardly adds the final point to the end.
Similarly, if I update it to have fewer points, it shortens the line and then slides the remainder over, instead of just reshaping the line where it is.
I understand why this happens, but I'm wondering if there's a way to create a more smooth transition.
You should have a look at this page and this github
.attrTween('d', function () {
return d3.interpolatePath(line(data), line(data2));
});
Have a look at this fiddle
I'm adapting Mike Bostock's point along path interpolation model to accept an array of n individual paths and interpolate along each consecutively. Being relatively new to D3 the code below shows as far as I've got, which is to run the point interpolation for both paths concurrently. Now I'm a bit stuck over how to restructure this to make the process consecutive (with just one moving object). Really I need to be able to stop between paths to listen for a mouseclick, but I can figure out that code once the structure is there. Most grateful for assistance.
Here's the jsfiddle.
Code for posterity:
<!DOCTYPE html>
<meta charset="utf-8">
<body>
<style>
path {
fill: none;
stroke: #000;
stroke-width: 3px;
}
circle {
stroke: #fff;
stroke-width: 3px;
}
</style>
<script type="text/javascript" src="http://d3js.org/d3.v3.js"></script><script>
var pathdata = [
[[240, 100],
[290, 200],
[340, 50]],
[[340, 50],
[90, 150],
[140, 50],
[190, 200]]
];
var svg = d3.select("body").append("svg")
.attr("width", 960)
.attr("height", 500);
var paths = svg.selectAll("path")
.data(pathdata)
.enter()
.append("path")
.attr("d", d3.svg.line())
.attr("id",function(d, i) { return "path" + i });
// plot path vertices
svg.selectAll(".point")
.data([].concat.apply([], pathdata))
.enter().append("circle")
.attr("r", 5)
.attr("fill", "red")
.attr("transform", function(d) { return "translate(" + d + ")"; });
// interpolate along path0
var circle = svg.append("circle")
.attr("r", 10)
.attr("fill", "steelblue")
.attr("transform", "translate(" + pathdata[0][1] + ")")
.transition()
.duration(4000)
.attrTween("transform", translateAlong(d3.select("#path0")[0][0]));
// interpolate along path1
var circle = svg.append("circle")
.attr("r", 10)
.attr("fill", "steelblue")
.attr("transform", "translate(" + pathdata[1][1] + ")")
.transition()
.duration(4000)
.attrTween("transform", translateAlong(d3.select("#path1")[0][0]));
function translateAlong(path) {
console.log(path);
var l = path.getTotalLength();
return function(d, i, a) {
return function(t) {
var p = path.getPointAtLength(t * l);
return "translate(" + p.x + "," + p.y + ")";
};
};
}
</script>
</body>
</html>
I'm also wondering if it might be better to format the input data along one of the following lines?
// 3rd field for path id
var points_alt1 = [
[240, 100, 0],
[290, 200, 0],
[340, 50, 0],
[340, 50, 1],
[90, 150, 1],
[140, 50, 1],
[190, 200, 1]
]
or..
// 3rd field for interpolation end-points
var points_alt2 = [
[240, 100, 0],
[290, 200, 0],
[340, 50, 1],
[340, 50, 0],
[90, 150, 0],
[140, 50, 0],
[190, 200, 1]
]
Create a function that takes as params a d3 selection of paths and an integer index of the path (within the selection) along which you want to animate. This function finds the appropriate path within that selection, starts up a transition of a circle along it, and subscribes to the 'end' event of the transition, at which point it triggers the next animation.
Here's the working fiddle
function animateSinglePath(selection, indexOfAnimated) {
indexOfAnimated = indexOfAnimated || 0;
// Create circle if doesn't already exist
// (achived by binding to single element array)
circle = svg.selectAll('.animated-circle').data([null])
circle.enter()
.append("circle")
.attr("class", "animated-circle")
.attr("r", 10)
.attr("fill", "steelblue")
selection.each(function(d, i) {
// find the path we want to animate
if(i == indexOfAnimated) {
// In this context, the "this" object is the DOM
// element of the path whose index, i equals the
// desired indexOfAnimated
path = d3.select(this)
// For style, make it dashed
path.attr('stroke-dasharray', '5, 5')
// Move circle to start pos and begin animation
console.log('Start animation', i)
circle
.attr("transform", "translate(" + d[0] + ")")
.transition()
.duration(2000)
.attrTween("transform", translateAlong(path.node()))
.each('end', function() {
console.log('End animation', i);
// For style, revert stroke to non-dashed
path.attr('stroke-dasharray', '')
// trigger the next animation by calling this
// function again
animateSinglePath(selection, indexOfAnimated + 1);
});
}
});
}
P.S
I wouldn't restructure the data as you proposed, because you need to have distinct SVG <path> elements – one for each "chapter" in the sequence. Having a distinct array for each of path, as you do now, is what enables you to create these <path>s via data() binding.
Depending on what you're trying to achieve, you may even want to further nest each path array, wrapping it with an object {}, to hold meta data about the path:
var pathData = [
{
name: "Crossing the Niemen",
points: [ [240, 100], [290, 200], [340, 50] ]
},
{
name: "March on Vilnius",
points: [ [340, 50], [90, 150], [140, 50], [190, 200] ]
},
{
name: "March on Moscow",
points: [ [190, 200], [70, 180], [30, 30], [350, 160] ]
}
];
I've made a chart with multiple y-axis values that are being plotted as circles. I now want to connect each y-axis value set with lines (a line chart basically).
I think my error lies in how I am attempting to define the y-axis line value:
var line = d3.svg.line()
.x(function(d) { return x(d.x); })
.y(function(d) { return y(d.y1, d.y2, d.y3); });
Here is my dataset:
var dataset = [
{x: d3.time.hour.utc.offset(now, -5), y1: 1, y2: 3, y3: 2},
{x: d3.time.hour.utc.offset(now, -4), y1: 2, y2: 2, y3: 4},
{x: d3.time.hour.utc.offset(now, -3), y1: 3, y2: 3, y3: 1},
{x: d3.time.hour.utc.offset(now, -2), y1: 4, y2: 1, y3: 2},
{x: d3.time.hour.utc.offset(now, -1), y1: 5, y2: 5, y3: 3},
{x: now, y1: 6, y2: 4, y3: 3},
];
And here is a complete example of my graph as of now: http://jsbin.com/edikeg/1/edit
I've read the line() method api reference but am not sure what else to try. If anyone could recommend the best approach to take to accomplish this, as well as any addition d3.js starting out tips or beginner resources to look into I would greatly appreciate it.
Thanks
First, your dataset is in a non-optimal format. A better format would be to create a single object for each line/point set:
var dataset = [
[
{x: d3.time.hour.utc.offset(now, -5), y: 1},
{x: d3.time.hour.utc.offset(now, -4), y: 2},
{x: d3.time.hour.utc.offset(now, -3), y: 3},
{x: d3.time.hour.utc.offset(now, -2), y: 4},
{x: d3.time.hour.utc.offset(now, -1), y: 5},
{x: now, y: 1}
],
[
// y2-values paired with x-values
],
[
// y3-values paired with x-values
]
];
Why, you ask? Because creating objects and groups of objects with d3 will be easier. You could also just write a map function to convert your current data into this format, if you need to.
Now, with your data in an easier format, the code for creating the lines and circles won't be dependent on the number of items in your dataset. Check out a working, rewritten version here:
http://jsbin.com/edikeg/3/edit
Basic explanation of what's happening:
For each item in the dataset, we create g.line, a pathContainer to hold each path and each of its circle points.
var pathContainers = svg.selectAll('g.line')
.data(dataset);
pathContainers.enter().append('g')
.attr('class', 'line');
Then, in each pathContainer, we create a path.
pathContainers.selectAll('path')
.data(function (d) { return [d]; }) // continues the data from the pathContainer
.enter().append('path')
.attr('d', d3.svg.line()
.x(function (d) { return xScale(d.x); })
.y(function (d) { return yScale(d.y); })
.interpolate('monotone')
);
Last, we create a circle for each point in the dataset for each pathContainer
pathContainers.selectAll('circle')
.data(function (d) { return d; })
.enter().append('circle')
.attr('cx', function (d) { return xScale(d.x); })
.attr('cy', function (d) { return yScale(d.y); })
.attr('r', 5);
Hope this helps!