I'm trying to create multiple lines on a line graph one at a time. I've created an object array of about 100 lines in the below format:
var allLines = [{type: "linear", values: [1000, 2000, 3000]}, {}, ... {}];
var line = d3.svg.line()
.defined(function (d) {
return d != null;
})
.x(function (d, i) {
return x(new Date(minYear + i, 1, 1));
})
.y(function (d) {
return y(d);
});
Now I want to draw each line, one at a time with a delay of about 250 milliseconds between each line. I've tried the below approach which I thought would work, but I must be missing something because it just waits 250ms and then draws all the lines.
svg.append('g')
.attr('class', 'lineGroup')
.selectAll('path')
.data(allLines)
.enter()
.append('path')
.attr('class', function (d) {
return d.type;
})
.style('visibility', 'hidden')
.attr('d', function (d) {
return line(d.values);
});
function foo(transition) {
transition
.style('visibility', 'visible');
}
d3.select('.lineGroup').selectAll('path').transition().delay(250).call(foo);
Your basic approach is right, you just need to adjust the delay dynamically such that the later lines are drawn later. At the moment, the delay applies to all lines. To make it dynamic, you can use something like
svg.append("g")
// etc
.transition()
.delay(function(d, i) { return i * 250; })
.style('visibility', 'visible');
You can also do everything in one call, no need for a separate one after creating the lines.
Related
For a project I am attempting to have three visualizations for data based on car stats, where if you hover over one, the others will show the affects of that hovering as well.
The first is a bar graph, the second is a scatterplot, and the third is a line graph. For the line graph I wanted to group by manufacturer so that I don't have a couple hundred lines on my line graph, as the plot coordinates on the x and y are acceleration and model year. The other two don't need to be grouped in this way because one of their axes is the manufacturer.
I have the interactions from the line graph to the other two working since there is no nesting on the bar or scatterplot, and both the scatterplot and the bar graph can affect each other perfectly fine, but since the data is nested for the line graph, I can't seem to figure out how to access it, as the way I was doing it for the other two (using filtering) does not seem to work.
Below I am first showing where I am trying to create interactions when the mouse hovers (this is for the bar graph), and below that I include how my line graph is set up to show how it works. All I want is to make the corresponding line stand out more from the others by thickening the stroke when I hover over the bar or plot (in the scatterplot), and then go back to the normal size upon moving my cursor.
I followed the tutorial on the D3 website for line graphs, so there shouldn't be anything particularly wrong with that code.
Creating the bars for the bar graph, the mouseover and mouseout are the important parts:
var path1 = svg1.selectAll("myRect")
.data(data)
.enter()
.append("rect")
.attr("x", x1(0.1) )
.attr("y", function(d) { return y1(d.Manufacturer); })
.attr("height", y1.bandwidth() )
.attr("width", function(d) { return x1(d.Cylinders); })
.attr("fill", function (d) {
return color1(d.Cylinders);
})
.on('mouseover', function (d, i) {
svg1.selectAll('rect')
.filter(function(f) {
return f.Manufacturer === d.Manufacturer;
})
.attr("fill", function (d) {
return color4(d.Cylinders);
})
svg2.selectAll('circle')
.filter(function(f) {
return f.Manufacturer === d.Manufacturer;
})
.attr('r', 9)
.attr("fill", function (d) {
return color5(d.Horsepower);
});
svg3.selectAll('path') //THIS IS THE LINE GRAPH
.filter(function(f) {
console.log(this)
return ; // <-------This is where I don't know what to return to just get one line
})
.attr("stroke-width", 7)
})
.on('mouseout', function (d, i) {
svg1.selectAll('rect')
.filter(function(f) {
return f.Manufacturer === d.Manufacturer;
})
.attr("fill", function (d) {
return color1(d.Cylinders);
});
svg2.selectAll('circle')
.filter(function(f) {
return f.Manufacturer === d.Manufacturer;
})
.attr('r', 5)
.attr("fill", function (d) {
return color2(d.Acceleration);
});
d3.selectAll('path') //DELESLECTING LINE GRAPH
.filter(function(f) {
return f.key === d.Manufacturer; //this is what I tried before but it doesn't work
})
.attr("stroke-width", 1.5)
});
Creating the line graph:
var sumstat = d3.nest()
.key(function(d) { return d.Manufacturer;})
.entries(data);
// Add X axis
var x3 = d3.scaleLinear()
.domain([69, 84])
.range([ 0, width3 ]);
svg3.append("g")
.attr("transform", "translate(0," + height3 + ")")
.call(d3.axisBottom(x3).ticks(5));
// Add Y axis
var y3 = d3.scaleLinear()
.domain([8, d3.max(data, function(d) { return +d.Acceleration; })])
.range([ height3, 0 ]);
svg3.append("g")
.call(d3.axisLeft(y3));
var div3 = d3.select("#my_div").append("div")
.attr("class", "#tool_tip")
.style("opacity", 0)
.style("font-size", "xx-large");
// color palette
var res = sumstat.map(function(d){ return d.key }) // list of group names
var color = d3.scaleOrdinal()
.domain(res)
.range(['darkolivegreen','darkred','palevioletred','indianred', 'hotpink'])
// Draw the line
svg3.selectAll(".line")
.data(sumstat)
.enter()
.append("path")
.attr("fill", "none")
.attr("stroke", function(d){ return color(d.key) })
.attr("stroke-width", 1.5)
.attr("d", function(d){
return d3.line()
.x(function(d) { return x3(d.ModelYear); })
.y(function(d) { return y3(+d.Acceleration); })
(d.values)
})
.on('mouseover', function (d, i) {
//highlight;
svg3.selectAll("path")
.attr("stroke-width", 0.9)
d3.select(this)
.attr("stroke", function(d){ return color(d.key)})
.attr("stroke-width", 6)
svg1.selectAll('rect')
.filter(function(f) {
return f.Manufacturer === d.key;
})
.attr("fill", function (d) {
return color4(d.Cylinders);
})
svg2.selectAll('circle')
.filter(function(f) {
return f.Manufacturer === d.key;
})
.attr('r', 9)
.attr("fill", function (d) {
return color5(d.Horsepower);
});
})
.on('mouseout', function (d, i) {
svg3.selectAll("path")
.attr("stroke-width", 1.5)
d3.select(this)
.attr("stroke", function(d){ return color(d.key)})
.attr("stroke-width", 1.5)
svg1.selectAll('rect')
.filter(function(f) {
return f.Manufacturer === d.key;
})
.attr("fill", function (d) {
return color1(d.Cylinders);
})
svg2.selectAll('circle')
.filter(function(f) {
return f.Manufacturer === d.key;
})
.attr('r', 5)
.attr("fill", function (d) {
return color2(d.Horsepower);
});
});
Any assistance I can get would be greatly appreciated!!
I think I may have figured out the problem. It would seem that trying to filter the paths causes an issue because the x and y axes are also technically lines, and thus have paths that are null. I tried
svg3.selectAll('path')
.filter(function(f) {
console.log(f)
if(f!=null)
return f.key === d.Manufacturer;
})
.attr("stroke-width",7)
In the .on('mouseover') function, and it seems to be working. The issue was the nulls, not the actual accessing of the keys.
Still taking suggestions if there is a better way to do this!
I'm trying to create a dynamic visualization using circles that 'spread' out over some amount of time and all of the circles have the same centerpoint.
I have a separate script that creates the circles and stores the data in a JSON file - the first circle in the JSON file is the smallest circle on top of the image linked above.
Please see code snippet below. Basically, the script appends the circle data into circles into an svg with visibility set to none. The script reveal the circles one by one.
In the appending function, I tried using the .lower() function to reverse the order that the circles are appended to the svg because if I were to append in the order that the JSON file is in, each consecutive circle would hide the one below it. But then the animation plots backwards, where the larger circle plots first.
In the revealing function, I then tried adding a similar '.lower()' function to the transition method so each consecutive circle would reveal behind the previously revealed circle but then the code breaks. I'm just at a loss here - any pointers would be much appreciated.
html,
body,
#svg {
background-color: #FFFFFF;
}
<html>
<head>
<meta charset="utf-8" />
<title>Visualizer</title>
</head>
<body>
<div>
<button onclick="plotStatically(0, 0, 'testingcircle.json')">Draw Static ►</button>
<button onclick="plotConsecutively(0, 0, 'testingcircle.json')">Draw Dynamic ►</button>
</div>
<script src="https://d3js.org/d3.v5.min.js" charset="utf-8"></script>
<script>
function plotConsecutively(x, y, nameFile) {
d3.json(nameFile).then(function(data) {
var svgHeight = window.innerHeight - 100;
var svgWidth = window.innerWidth - 10;
var svg = d3.select('body').append('svg')
.attr('width', svgWidth)
.attr('height', svgHeight);
svg.selectAll("circle")
.data(data)
.enter()
.append('circle')
.attr('r', function(d) {
return d.r;
})
.attr('cx', function(d) {
return d.cx + x;
})
.attr('cy', function(d) {
return d.cy + y;
})
.attr('fill', function(d) {
return d.fill;
})
.attr('visibility', 'hidden')
svg.selectAll("circle")
.transition()
.delay(function(d, i) {
return 3.4 * i;
})
.duration(10)
.attr('visibility', 'visible');
})
}
function plotStatically(x, y, nameFile) {
d3.json(nameFile).then(function(data) {
var svgHeight = window.innerHeight - 100;
var svgWidth = window.innerWidth - 10;
var svg = d3.select('body').append('svg')
.attr('width', svgWidth)
.attr('height', svgHeight);
svg.selectAll("circle")
.data(data)
.enter()
.append('circle')
.attr('r', function(d) {
return d.r;
})
.attr('cx', function(d) {
return d.cx;
})
.attr('cy', function(d) {
return d.cy;
})
.attr('fill', function(d) {
return d.fill;
});
})
}
</script>
</body>
</html>
I think you were pretty much there.
As you said, the larger circles need to be appended to the svg first so that they don't block out the smaller circles beneath them. I think this is most easily done simply by reversing the order of the data array just after you get the results of the json file:
d3.json(nameFile).then(function(data) {
data = data.reverse();
...
Then, in order to show the circles from the inside out, you can change your delay function so that the items at the end of the array that you want to show first (the smaller circles) have the smallest delay, and the items at the beginning of the array that you want to show last (the larger circles) have the largest delay.
The third argument to the delay() function is the NodesList containing all the selected DOM elements, so you can use the length property of that array in your calculations.
...
.delay(function(d, i, circleNodes) {
return 3.4 * ((circleNodes.length - 1) - i);
})
...
let data = [
{"r":5,"cx":100,"cy":100,"fill":"red"}, {"r":10,"cx":100,"cy":100,"fill":"magenta"},{"r":15,"cx":100,"cy":100,"fill":"orange"},{"r":20,"cx":100,"cy":100,"fill":"green"},{"r":25,"cx":100,"cy":100,"fill":"blue"}
];
data = data.reverse();
function plotConsecutively() {
var svg = d3.select('#svg')
.append('svg')
.attr('width', 200)
.attr('height', 200);
svg.selectAll("circle")
.data(data)
.enter()
.append('circle')
.attr('r', function(d) {
return d.r;
})
.attr('cx', function(d) {
return d.cx;
})
.attr('cy', function(d) {
return d.cy;
})
.attr('fill', function(d) {
return d.fill;
})
.attr('visibility', 'hidden')
svg.selectAll('circle')
.transition()
.delay(function(d, i, nodes) {
return 150 * ((nodes.length - 1) - i);
})
.duration(10)
.attr('visibility', 'visible');
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<button onclick="plotConsecutively()">Draw Dynamic ►</button>
<div id="svg"></div>
I created this chart using D3 V5. Also, I have attached the sample data on the fiddle you can view by clicking here.
I've included the tick function code block which appends new domains for x and y scales and line/data on the path to slide left:
When the tick function executes, the line sort of rebuilds which makes it look like it bounces.
How can it be smooth, without a bounce at all when it rebuilds the line?
var tr = d3
.transition()
.duration(obj.tick.duration)
.ease(d3.easeLinear);
function tick() {
return setInterval(function() {
var newData = [];
var tickFunction = obj.tick.fnTickData;
if (tickFunction !== undefined && typeof tickFunction === "function") {
newData = tickFunction();
for (var i = 0; i < newData.length; i++) {
obj.data.push(newData[i]);
}
}
if (newData.length > 0) {
var newMaxDate, newMinDate, newDomainX;
if (isKeyXDate) {
newMaxDate = new Date(
Math.max.apply(
null,
obj.data.map(function(e) {
return new Date(e[obj.dataKeys.keyX]);
})
)
);
newMinDate = new Date(
Math.min.apply(
null,
obj.data.map(function(e) {
return new Date(e[obj.dataKeys.keyX]);
})
)
);
newDomainX = [newMinDate, newMaxDate];
} else {
newDomainX = [
d3.min(obj.data, function(d) {
return d[obj.dataKeys.keyX];
}),
d3.max(obj.data, function(d) {
return d[obj.dataKeys.keyX];
})
];
}
// update the domains
//x.domain([newMinDate, newMaxDate]);
if (obj.tick.updateXDomain) {
newDomainX = obj.tick.updateXDomain;
}
x.domain(newDomainX);
if (obj.tick.updateYDomain) {
y.domain(obj.tick.updateYDomain);
}
path.attr("transform", null);
// slide the line left
if (obj.area.allowArea) {
areaPath.attr("transform", null);
areaPath
.transition()
.transition(tr)
.attr("d", area);
}
path
.transition()
.transition(tr)
.attr("d", line);
svg
.selectAll(".x")
.transition()
.transition(tr)
.call(x.axis);
svg
.selectAll(".y")
.transition()
.transition(tr)
.call(y.axis);
// pop the old data point off the front
obj.data.shift();
}
}, obj.tick.tickDelay);
}
this.interval = tick();
That bounce is actually the expected result when you transition the d attribute, which is just a string.
There are several solutions here. Without refactoring your code too much, a simple one is using the pathTween function written by Mike Bostock in this bl.ocks: https://bl.ocks.org/mbostock/3916621. Here, I'm changing it a little bit so you can pass the datum, like this:
path.transition()
.transition(tr)
.attrTween("d", function(d) {
var self = this;
var thisd = line(d);
return pathTween(thisd, 1, self)()
})
Here is the forked plunker: https://plnkr.co/edit/aAqpdSb9JozwHsErpqa9?p=preview
As Gerardo notes, transitioning the d attribute of the path won't work very well unless you modfiy the approach. Here's a simple example of the sort of wiggle/bouncing that will arise if simply updating the d attribute of the path:
Pᴏɪɴᴛs ᴛʀᴀɴsɪᴛɪᴏɴɪɴɢ ᴀᴄʀᴏss sᴄʀᴇᴇɴ, ᴡɪᴛʜ ᴘᴀᴛʜ ᴛʀᴀɴsɪᴛɪᴏɴɪɴɢ ғʀᴏᴍ ᴏɴᴇ ᴅᴀᴛᴀ sᴇᴛ ᴛᴏ ᴛʜᴇ ɴᴇxᴛ.
The above behavior is noted by Mike Bostock in a short piece here, and here's a snippet reproducing the above animation:
var n = 10;
var data = d3.range(n).map(function(d) {
return {x: d, y:Math.random() }
})
var x = d3.scaleLinear()
.domain(d3.extent(data, function(d) { return d.x; }))
.range([10,490])
var y = d3.scaleLinear()
.range([290,10]);
var line = d3.line()
.x(function(d) { return x(d.x); })
.y(function(d) { return y(d.y); })
var svg = d3.select("body")
.append("svg")
.attr("width",500)
.attr("height", 400)
.append("g");
var path = svg.append("path")
.datum(data)
.attr("d", line);
var points = svg.selectAll("circle")
.data(data, function(d) { return d.x; })
.enter()
.append("circle")
.attr("cx", function(d) { return x(d.x); })
.attr("cy", function(d) { return y(d.y); })
.attr("r", 5);
function tick() {
var transition = d3.transition()
.duration(1000);
var newPoint = {x:n++, y: Math.random() };
data.shift()
data.push(newPoint);
x.domain(d3.extent(data,function(d) { return d.x; }))
points = svg.selectAll("circle").data(data, function(d) { return d.x; })
points.exit()
.transition(transition)
.attr("cx", function(d) { return x(d.x); })
.attr("cy", function(d) { return y(d.y); })
.remove();
points.enter().append("circle")
.attr("cx", function(d) { return x(d.x)+30; })
.attr("cy", function(d) { return y(d.y); })
.merge(points)
.transition(transition)
.attr("cx", function(d) { return x(d.x); })
.attr("r", 5);
path.datum(data)
.transition(transition)
.attr("d", line)
.on("end", tick);
}
tick();
path {
fill: none;
stroke: black;
stroke-width: 2;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
One solution to this wiggle/bounce is:
add an additional point(s) to the data,
redraw the line with the recently added to data array
find out the next extent of the data
transition the line to the left
update the scale and transition the axis
remove the first data point(s)
This is also proposed in Mike's article that I've linked to. Here would be a basic implementation with your code:
I've avoided a setInterval function by recursively calling the function at the end of the last transition:
function slide() {
// Stop any ongoing transitions:
d3.selectAll().interrupt();
// A transition:
var transition = d3.transition()
.duration(2000)
.ease(d3.easeLinear)
// 1. add an additional point(s) to the data
var newData = obj.tick.fnTickData();
obj.data.push(...newData);
// 2. redraw the line with the recently added to data array
path.datum(obj.data)
areaPath.datum(obj.data)
// Redraw the graph, without the translate, with less data:
path.attr("transform","translate(0,0)")
.attr("d", line)
areaPath.attr("transform","translate(0,0)")
.attr("d", area)
// 3. find out the next extent of the data
// Assuming data is in chronological order:
var min = obj.data[newData.length][obj.dataKeys.keyX];
var max = obj.data[obj.data.length-1][obj.dataKeys.keyX];
// 4. transition the line to the left
path.datum(obj.data)
.transition(transition)
.attr("transform", "translate("+(-x(new Date(min)))+",0)");
areaPath.datum(obj.data)
.transition(transition)
.attr("transform", "translate("+(-x(new Date(min)))+",0)");
// 5. update the scale and transition the axis
x.domain([new Date(min),new Date(max)])
// Update the xAxis:
svg.selectAll('.x')
.transition(transition)
.call(x.axis)
.on("end",slide); // Trigger a new transition at the end.
// 6. remove the first data point(s)
obj.data.splice(0,newData.length)
}
slide();
Here's an updated plunkr.
I have a demo here
Its a line bar chart using D3 in an Angular app.
I want the chart to be responsive so when the page is resized the chart width will increase and the height will be stay the same.
I'm doing this by capturing the window resize and then calling the function that draws the chart.
This works for the axis but I cant get the line and points to redraw.
I think it's to do with the way I'm trying to us the update pattern
How can I use the update pattern to redraw this line graph
const that = this;
const valueline = d3.line()
.x(function (d, i) {
return that.x(d.date) + 0.5 * that.x.bandwidth();
})
.y(function (d) {
return that.y(d.value);
});
this.x.domain(data.map((d: any) => d.date));
this.y.domain(d3.extent(data, function (d) {
return d.value
}));
const thisLine = this.chart.append("path")
.data([data])
.attr("class", "line")
.attr("d", valueline);
const totalLength = thisLine.node().getTotalLength();
thisLine.attr("stroke-dasharray", totalLength + " " + totalLength)
.attr("stroke-dashoffset", totalLength);
thisLine.transition()
.duration(1500)
.attr("stroke-dashoffset", 0)
let circle = this.chart.selectAll("line-circle")
.data(data);
circle = circle
.enter()
.append("circle")
.attr("class", "line-circle")
.attr("r", 4)
.attr("cx", function (d) {
return that.x(d.date) + 0.5 * that.x.bandwidth();
})
.attr("cy", function (d) {
return that.y(d.value);
})
circle
.attr("r", 4)
.attr("cx", function (d) {
return that.x(d.date) + 0.5 * that.x.bandwidth();
})
.attr("cy", function (d) {
return that.y(d.value);
})
circle
.exit()
.remove()
You have problems in both circles' selection and the line selection.
The circles' selection:
You're selecting "line-circle". Instead of that, you have to select by class: ".line-circle";
You're reassigning the circle selection:
circle = circle.enter()//etc...
Don't do that, otherwise circle will point to the enter selection, not to the update selection anymore. Just do:
circle.enter()//etc...
The path:
You're appending a new path every time you call the function. Don't do that. Instead, select the existing path and change its d attribute, or append a new path if there is none. Both behaviours can be achieved with this code:
let thisLine = this.chart.selectAll(".line")
.data([data]);
thisLine = thisLine.enter()
.append("path")
.attr("class", "line")
.merge(thisLine)
.attr("d", valueline);
Here is your forked code: https://stackblitz.com/edit/basic-scatter-mt-vvdxqr?file=src/app/bar-chart.ts
I've got a line graph with 159 lines. When the graph first loads, I want them all to graph as a straight line, y=5. That works. Then I want them to beautifully transition to their bumpy selves.
Well, my dang transition is jerky, not beautiful. I think I need a remove() somewhere in here.
Graph is here. Ignore the drop-downs; they're not part of the original load.
http://bl.ocks.org/greencracker/raw/f37dc463afa15bf17d91/
I think the key part may be in here:
var line = d3.svg.line()
.interpolate("basis")
.x(function (d) {return x(d.date); })
.y(function (d) {return y(d.rate) ; });
var flat_line = d3.svg.line()
.interpolate("basis")
.x(function (d, i) {return x(d.date); })
.y(function (d) {return y(5) ; });
Then further down:
function lineAnimate(selection) { // selection must be paths
selection.transition().duration(1150).ease("linear")
.attr("d", function (d) {return line(d.values);})
.style("opacity", 1)
.style("stroke", "red")
}
var counties = svg.selectAll(".county")
.data(data, function (d) {return d.key;})
.enter().append("g")
.attr("class", function (d) {return d.key + " " + d.wia + " county";})
.text(function (d) {return d.key});
counties.append("path")
.attr("class", function (d) {return d.key + " " + d.wia + " county" + " line";})
.attr("id", function (d) {return "a" + d.wia;})
.attr("d", function (d) {return flat_line(d.values); }) // flat line y=5
.attr("stroke", "gray")
.attr("stroke-width", "1px")
.style("opacity", 0.0)
counties.selectAll("path.line")
.call(lineAnimate)
It's just a lot of work to animate 159 lines simultaneously, and there's probably not much to do about that.
However, the SVG renderer is further bogged down by having to determine the resulting fills of all these overlapping semi-transparent lines. I suspect you can get a significant boost if you do away with the opacity change.
Finally, you might be able to improve the "line morph" animation by staggering the animations; i.e. the first line would begin (and finish) its animation a bit before the 2nd line, then the 3rd, etc. This way, only a subset of the lines is animating at any given point.
You'd do this by adding something like .delay( function(d, i) { return i * 100; }) after the .transition() call. Might not be the effect you're going for though. Just a thought...