Date and time transition from data with line - javascript

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.

Related

Using General update pattern in line graph

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

Appending clip path in d3js v4

I'm attempting to upgrade an old v3 chart to v4 and got stuck at trying to append clipPath.
At first I thought that the way to append clip path was changed in v4 somehow but It appears to be the same as before.
My reference is Mike Bostocks chart.
And here's the progress I've made so far.
One thing i've tried was basically to copy/paste the line path logic and change:
line(d.values) to area(d.values)
I get no errors from this so I'm not sure why it isn't working.
The issue was that the y scale was not being calculated within the same environment as the clip path - giving the clip path a height of 0. I moved the clip path to the update function. You can probably get a more elegant update of the clip path, but I added a line at the top to remove the existing clip path so a new one could be appended: https://plnkr.co/edit/KQC1A70b4O5fNtHrQEkn?p=preview
function update() {
d3.selectAll('clipPath').remove();
VALUE = d3.select('#selectbox').property('value');
d3.csv("data.csv", function(d, _, columns) {
d.date = parseDate(d.date);
for (var i = 1, n = columns.length, c; i < n; ++i)
d[c = columns[i]] = +d[c];
return d;
}, function(error, data) {
if (error) throw error;
baseValue = data[0]["Category" + VALUE];
console.log(baseValue)
var keys = data.columns.slice(1,2);
var copy = [];
keys.forEach(function(t) {
t = t.slice(0, -2) // Slice last two letters
copy.push(t) // Push sliced strings into copy array
});
var cities = copy.map(function(id) {
return {
id: id,
values: data.map(function(d) {
return {date: d.date, city: d[id+VALUE] / baseValue};
})
};
});
x.domain(d3.extent(data, function(d) { return d.date; }));
y.domain([
d3.min(cities, function(c) {
return d3.min(c.values, function(d) {
return d.city;
});
}),
d3.max(cities, function(c) {
return d3.max(c.values, function(d) {
return d.city;
});
})
]);
defs.append("clipPath")
.attr("id", "clip-above")
.append("rect")
.attr("width", width)
.attr("height", y(1));
area.y0(y(1));
yAxis.tickValues(d3.scaleLinear()
.domain(y.domain())
.ticks(20));
gY.transition().duration(durations).call(yAxis);
gY.selectAll(".tick")
.classed("tick--one", function(d) { return Math.abs(d - 1) < 1e-6; });
g.selectAll(".axis.axis--x").transition()
.duration(durations)
.call(xAxis);
// ========= Above Clip =========
var above = g.selectAll(".above")
.data(cities);
above = above
.enter()
.append("path")
.attr("clip-path", "url(#clip-above)")
.attr("class", "area area--above above")
.merge(above);
above.transition()
.duration(durations)
.attr("d", function(d) {return area(d.values)} );
// ========= Line Path =========
var cityLine = g.selectAll(".cities")
.data(cities);
cityLine = cityLine
.enter()
.append("path")
.attr("class", "line cities")
.merge(cityLine);
cityLine.transition()
.duration(durations)
.attr("d", function(d) { return line(d.values) });
afterLoad();
});

D3 stops plotting points when data source is modified

I am plotting points on a UK map using D3 off a live data stream. When the data points exceed 10,000 the browser becomes sluggish and the animation is no longer smooth. So I modify the dataPoints array to keep only the last 5000 points.
However when I modify the dataPoints the first time using splice() D3 stops rendering any new points. The old points gradually disappear (due to a transition) but there are no new points. I am not sure what I am doing wrong here.
I have simulated the problem by loading data of a CSV as well storing it in memory and plotting them at a rate of 1 point every 100ms. Once the number of dots goes above 10 I splice to retain the last 5 points. I see the same behaviour. Can someone review the code and let me know what I am doing wrong?
Setup and the plotting function:
var width = 960,
height = 1160;
var dataPoints = []
var svg = d3.select("#map").append("svg")
.attr("width", width)
.attr("height", height);
var projection = d3.geo.albers()
.center([0, 55.4])
.rotate([4.4, 0])
.parallels([40, 70])
.scale(5000)
.translate([width / 2, height / 2]);
function renderPoints() {
var points = svg.selectAll("circle")
.data(dataPoints)
points.enter()
.append("circle")
.attr("cx", function (d) {
prj = projection([d.longitude, d.latitude])
return prj[0];
})
.attr("cy", function (d) {
prj = projection([d.longitude, d.latitude])
return prj[1];
})
.attr("r", "4px")
.attr("fill", "blue")
.attr("fill-opacity", ".4")
.transition()
.delay(5000)
.attr("r", "0px")
}
/* JavaScript goes here. */
d3.json("uk.json", function(error, uk) {
if (error) return console.error(error);
console.log(uk);
var subunits = topojson.feature(uk, uk.objects.subunits);
var path = d3.geo.path()
.projection(projection);
svg.selectAll(".subunit")
.data(subunits.features)
.enter().append("path")
.attr("class", function(d) { return "subunit " + d.id })
.attr("d", path);
svg.append("path")
.datum(topojson.mesh(uk, uk.objects.subunits, function(a,b) {return a!== b && a.id !== 'IRL';}))
.attr("d", path)
.attr("class", "subunit-boundary")
svg.append("path")
.datum(topojson.mesh(uk, uk.objects.subunits, function(a,b) {return a=== b && a.id === 'IRL';}))
.attr("d", path)
.attr("class", "subunit-boundary IRL")
svg.selectAll(".place-label")
.attr("x", function(d) { return d.geometry.coordinates[0] > -1 ? 6 : -6; })
.style("text-anchor", function(d) { return d.geometry.coordinates[0] > -1 ? "start": "end"; });
svg.selectAll(".subunit-label")
.data(topojson.feature(uk, uk.objects.subunits).features)
.enter().append("text")
.attr("class", function(d) { return "subunit-label " + d.id })
.attr("transform", function(d) { return "translate(" + path.centroid(d) + ")"; })
.attr("dy", ".35em")
.text(function(d) { return d.properties.name; })
// function applyProjection(d) {
// console.log(d);
// prj = projection(d)
// console.log(prj);
// return prj;
// }
lon = -4.6
lat = 55.45
dataPoints.push([lon,lat])
renderPoints()
});
Function to cleanup old points
var cleanupDataPoints = function() {
num_of_elements = dataPoints.length
console.log("Pre:" + num_of_elements)
if(num_of_elements > 10) {
dataPoints = dataPoints.splice(-5, 5)
}
console.log("Post:" + dataPoints.length)
}
Loading data from CSV and plotting at a throttled rate
var bufferedData = null
var ptr = 0
var renderNext = function() {
d = bufferedData[ptr]
console.log(d)
dataPoints.push(d)
ptr++;
renderPoints()
cleanupDataPoints()
if(ptr < bufferedData.length)
setTimeout(renderNext, 100)
}
d3.csv('test.csv', function (error, data) {
bufferedData = data
console.log(data)
setTimeout(renderNext, 100)
})
In the lines
points = svg.selectAll("circle")
.data(dataPoints)
points.enter() (...)
d3 maps each element in dataPoints (indexed from 0 to 5000) to the circle elements (of which there should be 5000 eventually). So from its point of view, there is no enter'ing data: there are enough circles to hold all your points.
To make sure that the same data point is mapped to the same html element after it changed index in its array, you need to use an id field of some sort attached to each of your data point, and tell d3 to use this id to map the data to elements, instead of their index.
points = svg.selectAll("circle")
.data(dataPoints, function(d){return d.id})
If the coordinates are a good identifier for your point, you can directly use:
points = svg.selectAll("circle")
.data(dataPoints, function(d){return d.longitude+" "+d.latitude})
See https://github.com/mbostock/d3/wiki/Selections#data for more details.

Override d3 index values in Force-Directed Graph

I am using a force directed graph to draw entity relationships for a company. What I would like to do is use my built in index value for the "index" of the node instead of the array index. How do I override the d3 index value that gets set? Fiddle - http://jsfiddle.net/thielcole/ed9noqw1/
var forceLinks = [];
var forceData = [];
d3.csv('AmazonExampleforiCharts.csv', function(d) {
return { // This is the index I would like to use
index: d.Node,
parent: d.ParentNode,
color: d.NodeColor,
level: d.Hierarchy_code,
business: d.Business_Name,
power: d.Decisionpower,
hover: d.HoverOverValue,
link: d.LinkVALUE
};
}, function(error, rows) {
forceData = rows;
$.each(forceData, function(i, d) {
// console.log(d.parent);
if (d.parent != "" && d.parent !== undefined) { // generating link information here, I have to subtract one initially to match the array index
forceLinks.push({source: parseInt(d.parent , 10) - 1 , target: parseInt(d.index , 10) - 1, value: parseInt(d.level , 10)});
}
console.log(d);
});
$.each(forceLinks, function(i, d) {
// console.log(d);
});
initiateChart();
});
function initiateChart() {
var height = 1000,
width = 1400;
var graphData = [];
var graphLinks = [];
graphData = forceData;
graphLinks = forceLinks;
var color = d3.scale.category20();
var svg = d3.select('.svg')
.attr('width', width)
.attr('height', height);
var force = d3.layout.force()
.charge(-120)
.linkDistance(30)
.size([width, height]);
// this is where it gets set
force.nodes(graphData)
.links(graphLinks)
.start();
var link = svg.selectAll('.link')
.data(graphLinks)
.enter().append('line')
.attr('class', 'link')
.style('stroke-width', function (d) {
return Math.sqrt(d.value);
});
var node = svg.selectAll('.node')
.data(graphData)
.enter().append('circle')
.attr('class', 'node')
.attr('r', 8)
.style('fill', function (d) {
return color(d.level);
})
.on('mouseover', function(d , i) {
d3.select(this).attr('r', 12);
var tooltip = d3.select('body')
.append('div')
.attr('class', 'tooltip')
.style('position','absolute')
.style('top', (d3.event.pageY - 10) + 'px')
.style('left' , (d3.event.pageX) + 'px' )
.style('z-index' , '10')
.text(d.hover);
console.log(d)
})
.on('click', function(d , i) {
showChildren(d);
})
.on('mouseout', function(d, i) {
d3.select(this).attr('r', 8);
d3.select('body').selectAll('.tooltip')
.remove();
})
.call(force.drag);
//Now we are giving the SVGs co-ordinates - the force layout is generating the co-ordinates which this code is using to update the attributes of the SVG elements
force.on("tick", function () {
link.attr("x1", function (d) {
return d.source.x;
})
.attr("y1", function (d) {
return d.source.y;
})
.attr("x2", function (d) {
return d.target.x;
})
.attr("y2", function (d) {
return d.target.y;
});
node.attr("cx", function (d) {
return d.x;
})
.attr("cy", function (d) {
return d.y;
})
});
}
var node = svg.selectAll('.node')
.data(graphData)
.enter().append('circle')
.attr('class', 'node')
.attr('r', 8)
.attr('id', function(d){return(d.index);})// use the data 'index' property as node 'id' attribute
.style('fill', function (d) {
return color(d.level);
})
.call(force.drag);
The 'index'-es will be stored in circles 'id' attributes as strings (not numbers):
You can add as much attributes as you want and recall them later. Actually, the id tag is not the best choice. The much better alternatives are: data-index, data-id, data-company etc.
PS:
The notation .attr('class', 'node') is not a mistake, but d3.js have special selection.classed(name[, value]) method for classes. I recommend you for attributes use native js arrays:
var styles = {
node: {stroke: "white", "stroke-width": 1.5, r: 8},
link: {stroke: "#999", "stroke-width": 0.6}
};
...
var node = svg.selectAll('.node')
...
.attr(styles.node)
...
By the way, in SVG fill is attribute.
DEMO: http://jsfiddle.net/3gw8vxa3/

Using data to produce scatterplot with d3 transition method

I'm trying to do something like this: http://bost.ocks.org/mike/nations/
However instead of the transitions on mouseover I want the transitions to display when I click on a button for each year in the timeline.
Some example data in a csv file:
time,name,xAxis,yAxis,radius,color
1990,America,10,20.2,30,black
1990,China,50,50,50,yellow
2000,Singapore,20,30,20,red
2010,China,60,50,50,yellow
2020,America,20,30,40,black
2020,Malaysia,60,5,10,orange
I'm new to javascript and d3 and am having trouble with the transitions. I want the circles to be unique to each name (America, China, Singapore, Malaysia) so that I will only have one circle per name. Currently new circles add when I click on the respective timeline buttons, but don't transit to new positions or exit.
Read data using d3.csv:
d3.csv("data.csv", function(dataset) {
var years = [];
data=dataset;
//create a button for each year in the timeline
dataset.forEach(function(d){
console.log(d.time);
//if not existing button for timeline
if($.inArray(d.time, years) == -1)
{
var button = document.createElement("button");
button.setAttribute("type", "button");
button.setAttribute("class", "btn btn-default");
button.setAttribute('onclick', 'update("'+d.time+'")');
var t = document.createTextNode(d.time);
button.appendChild(t);
$("#timeline").append(button);
years.push(d.time);
}
})
//create circles for the first year
svg.selectAll("circles")
.data(dataset.filter(function(d) { return d.time == d3.min(years);}, function(d) { return d.name; }))
.enter()
.append("circle")
//.filter(function(d){ return d.time == d3.min(years); })
.attr("cx", function (d) { return d.xAxis *10; })
.attr("cy", function (d) { return d.yAxis; })
.style("fill", function(d) { return d.color; })
.transition()
.duration(800)
.attr("r", function(d) { return d.radius});
});
My update function:
function update(year){
var circle = svg.selectAll("circles")
.data(data.filter(function(d){return d.time == year;}), function(d) { return d.name; });
//update
circle.attr("class", "update")
.filter(function(d){ return d.time == year; })
.transition()
.duration(800)
.attr("cx", function (d) { return d.xAxis *10; })
.attr("cy", function (d) { return d.yAxis; })
.attr("r", function(d) { return d.radius});
//enter
circle.enter().append("circle")
.filter(function(d){ return d.time == year; })
.attr("cx", function (d) { return d.xAxis *10; })
.attr("cy", function (d) { return d.yAxis; })
.style("fill", function(d) { return d.color; })
.attr("r", function(d) { return d.radius});
//exit
circle.exit()
.remove();
}
Can someone point me in the right direction? Thanks.
svg.selectAll("circles") is invalid and should become svg.selectAll("circle") (singularize "circles").
As you have it currently, with "circles", it yields an empty selection, so d3 assumes all your data is bound to non-existent circles, and therefore the .enter() selection is always full (rather than being full only at the first render).
Next, in the section labled //update, you shouldn't need to do any filtering. The .data() binding you're doing to a filtered array should take care of this for you.
Also, the section labeled //create circles for the first year is unnecessary, and probably should be removed to eliminate side effect bugs. The update() function, assuming it's working fine, should take care of this for you.

Categories

Resources