I am trying to edit this d3 example.
More specifically, I will try to apply the pause-resume controls of a pause resume guide in addition with a control bar like this we have under videos. At the end I imagine to have something like this:
How can apply the pause resume control in the beginning?
Here's a quick implementation. The pause essentially cancels the current transition and the play resumes it based on time/position it was paused:
var pauseValues = {
lastT: 0,
currentT: 0
};
function transition() {
circle.transition()
.duration(duration - (duration * pauseValues.lastT)) // take into account any pause
.attrTween("transform", translateAlong(path.node()))
.each("end", function(){
pauseValues = {
lastT: 0,
currentT: 0
};
transition()
});
}
function translateAlong(path) {
var l = path.getTotalLength();
return function(d, i, a) {
return function(t) {
t += pauseValues.lastT; // was it previously paused?
var p = path.getPointAtLength(t * l);
pauseValues.currentT = t; // just in case they pause it
return "translate(" + p.x + "," + p.y + ")";
};
};
}
d3.select('button').on('click',function(d,i){
var self = d3.select(this);
if (self.text() == "Pause"){
self.text('Play');
circle.transition()
.duration(0);
setTimeout(function(){
pauseValues.lastT = pauseValues.currentT; // give it a bit to stop the transition
}, 100);
}else{
self.text('Pause');
transition();
}
});
transition();
Example here.
Related
I have two elements I need to render and a context of the big picture I am trying to achieve (a complete dashboard).
One is a chart that renders fine.
$scope.riskChart = new dc.pieChart('#risk-chart');
$scope.riskChart
.width(width)
.height(height)
.radius(Math.round(height/2.0))
.innerRadius(Math.round(height/4.0))
.dimension($scope.quarter)
.group($scope.quarterGroup)
.transitionDuration(250);
The other is a triangle, to be used for a more complex shape
$scope.openChart = d3.select("#risk-chart svg g")
.enter()
.attr("width", 55)
.attr("height", 55)
.append('path')
.attr("d", d3.symbol('triangle-up'))
.attr("transform", function(d) { return "translate(" + 100 + "," + 100 + ")"; })
.style("fill", fill);
On invocation of render functions, the dc.js render function is recognized and the chart is seen, but the d3.js render() function is not recognized.
How do I add this shape to my dc.js canvas (an svg element).
$scope.riskChart.render(); <--------------Works!
$scope.openChart.render(); <--------------Doesn't work (d3.js)!
How do I make this work?
EDIT:
I modified dc.js to include my custom chart, it is a work in progress.
dc.starChart = function(parent, fill) {
var _chart = {};
var _count = null, _category = null;
var _width, _height;
var _root = null, _svg = null, _g = null;
var _region;
var _minHeight = 20;
var _dispatch = d3.dispatch('jump');
_chart.count = function(count) {
if(!arguments.length)
return _count;
_count = count;
return _chart;
};
_chart.category = function(category) {
if(!arguments.length)
return _category
_category = category;
return _chart;
};
function count() {
return _count;
}
function category() {
return _category;
}
function y(height) {
return isNaN(height) ? 3 : _y(0) - _y(height);
}
_chart.redraw = function(fill) {
var color = fill;
var triangle = d3.symbol('triangle-up');
this._g.attr("width", 55)
.attr("height", 55)
.append('path')
.attr("d", triangle)
.attr("transform", function(d) { return "translate(" + 25 + "," + 25 + ")"; })
.style("fill", fill);
return _chart;
};
_chart.render = function() {
_g = _svg
.append('g');
_svg.on('click', function() {
if(_x)
_dispatch.jump(_x.invert(d3.mouse(this)[0]));
});
if (_root.select('svg'))
_chart.redraw();
else{
resetSvg();
generateSvg();
}
return _chart;
};
_chart.on = function(event, callback) {
_dispatch.on(event, callback);
return _chart;
};
_chart.width = function(w) {
if(!arguments.length)
return this._width;
this._width = w;
return _chart;
};
_chart.height = function(h) {
if(!arguments.length)
return this._height;
this._height = h;
return _chart;
};
_chart.select = function(s) {
return this._root.select(s);
};
_chart.selectAll = function(s) {
return this._root.selectAll(s);
};
function resetSvg() {
if (_root.select('svg'))
_chart.select('svg').remove();
generateSvg();
}
function generateSvg() {
this._svg = _root.append('svg')
.attr({width: _chart.width(),
height: _chart.height()});
}
_root = d3.select(parent);
return _chart;
}
I think I confused matters by talking about how to create a new chart, when really you just want to add a symbol to an existing chart.
In order to add things to an existing chart, the easiest thing to do is put an event handler on its pretransition or renderlet event. The pretransition event fires immediately once a chart is rendered or redrawn; the renderlet event fires after its animated transitions are complete.
Adapting your code to D3v4/5 and sticking it in a pretransition handler might look like this:
yearRingChart.on('pretransition', chart => {
let tri = chart.select('svg g') // 1
.selectAll('path.triangle') // 2
.data([0]); // 1
tri = tri.enter()
.append('path')
.attr('class', 'triangle')
.merge(tri);
tri
.attr("d", d3.symbol().type(d3.symbolTriangle).size(200))
.style("fill", 'darkgreen'); // 5
})
Some notes:
Use chart.select to select items within the chart. It's no different from using D3 directly, but it's a little safer. We select the containing <g> here, which is where we want to add the triangle.
Whether or not the triangle is already there, select it.
.data([0]) is a trick to add an element once, only if it doesn't exist - any array of size 1 will do
If there is no triangle, append one and merge it into the selection. Now tri will contain exactly one old or new triangle.
Define any attributes on the triangle, here using d3.symbol to define a triangle of area 200.
Example fiddle.
Because the triangle is not bound to any data array, .enter() should not be called.
Try this way:
$scope.openChart = d3.select("#risk-chart svg g")
.attr("width", 55)
.attr("height", 55)
.append('path')
.attr("d", d3.symbol('triangle-up'))
.attr("transform", function(d) { return "translate(" + 100 + "," + 100 + ")"; })
.style("fill", fill);
Trying to implement object constancy. The concept is to update the DOM with text using progressive phrases within the text as data.
////////////////////////////////////////////////////////////////////////
// We need a way to change the data to illustrate the update pattern.
// We use lyrics to do this. Below is code for updating the data.
////////////////////////////////////////////////////////////////////////
var data = [];
function lyricIterator(phrases) {
var nextIndex = 0;
return {
next: function(){
if (nextIndex >= (phrases.length - 1)) {
nextIndex = 0;
debugger;
} else {
nextIndex = nextIndex + 1;
}
// console.log(phrases.slice(nextIndex - 1, nextIndex + 2));
return phrases.slice(nextIndex - 1, nextIndex + 2);
}
}
}
var lyrics = [
{i : 0, phrase : "Row, row, row your boat,"},
{i : 1, phrase : "Gently down the stream."},
{i : 2, phrase : "Merrily, merrily, merrily, merrily,"},
{i : 3, phrase : "Life is but a dream."}
]
// Instantiate an iterator for our lyrics
var rrryb = lyricIterator(lyrics)
/////////////
// Set up
//////////////
var width = 960,
height = 500;
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(32," + (height / 2) + ")");
//////////////////////////
// Redraw the elements
/////////////////////////
function update() {
data = rrryb.next()
var phrases = svg.selectAll("phrases")
.data(data, function(d) { return d.i; });
console.log("Update")
console.log(phrases)
console.log("Enter")
console.log(phrases.enter())
console.log("Exit")
console.log(phrases.exit())
phrases.exit().remove();
// UPDATE
// Update old elements as needed.
phrases.attr("class", "update");
phrases.enter().append("text")
.attr("class", "enter")
.attr("dy", function(d, i) { return i * 32; })
.attr("dx", ".35em");
phrases.text(function(d) { return d.phrase; })
}
////////////////////////////////////////////////////
// Register the function to run at some interval
///////////////////////////////////////////////////
setInterval(function() {
update()
}, 1500);
I get this as output to the console (enter and update with all the elements everytime):
And the output is just the text stacked on one another
Instead of this:
var phrases = svg.selectAll("phrases")//there is no DOM as phrases it will return an empty selection always.
.data(data, function(d) { return d.i; });
it should be this:
var phrases = svg.selectAll("text")//select all the text
.data(data, function(d) { return d.i; });
Reason: This will return an empty selection always svg.selectAll("phrases") that is why its appending all the time.
In second case it will return a selection of text DOMs.
working code here
I would like to visualize a circle with a random colour at a random x and y coordinate, then add an extra colourful circle at a random location every second.
I am using d3.timer to run a function that appends x and y coordinates to my dataset object, which is bound to all of the circle objects. When I print the dataset object, I can see that my function does in fact append the new x and y coordinates to my dataset object. However, the visualization does not update with the new circles. How can I add a new circle every second?
Relevant functions below:
var reshuffleData = function(){
for (var i=0; i<5; i++){
console.log('Reshuffling')
dataset.push({x: randomXPosition(), y: randomYPosition()})
}
console.log(dataset)
return true
}
d3.timer(reshuffleData, 10);
Full jsfiddle here: http://jsfiddle.net/d74Le5xk/
It wasn't not working because d3.timer is used incorrectly. As d3.timer just takes a function to paint next animation frame. We do not control when the this function will be called but it will be called most likely (1/ frames per second seconds). Where FPS may change every second.
If you want to do something periodically use setInterval also you need to redraw the circles once the dataset size is changed.
Following is the jsfiddle link for the working code.
http://jsfiddle.net/d74Le5xk/3/
Also attaching the code here for reference.
HTML
<svg class='canvas'></svg>
Javascript
(function () {
var width = 420, height = 200;
var randomXPosition = function(d){
return Math.random() * width;
}
var randomYPosition = function(d){
return Math.random() * height;
}
var dataset = [];
var circleBatchSize = 5;
var maxCircleCount = 100;
for (var i=0; i < circleBatchSize; i++){
dataset.push({x: randomXPosition(), y: randomYPosition()})
}
var testInterval = null;
var reshuffleData = function(){
for (var i=0; i<circleBatchSize; i++){
dataset.push({x: randomXPosition(), y: randomYPosition()})
//return true;
}
console.log('Reshuffled ' + dataset.length)
console.log(dataset)
if(dataset.length > maxCircleCount) {
clearInterval(testInterval);
}
}
console.log(dataset);
var colours = ['#FDBB30', '#EE3124', '#EC008C', '#F47521', '#7AC143', '#00B0DD'];
var randomColour = function() {
return colours[Math.floor(Math.random() * colours.length)];
}
//d3.timer(reshuffleData, 0, 5000);
testInterval = window.setInterval(reshuffleData, 2000);
var canvas = d3.select('.canvas')
.attr('width', width)
.attr('height', height)
.style('background-color', 'black');
var datasetOldLength = 0;
function drawCircles() {
if(datasetOldLength === dataset.length ) {
return;
}
datasetOldLength = dataset.length;
var circles = canvas.selectAll('circle')
.data(dataset)
.enter()
.append('circle')
.style('r', 20)
.style('fill', randomColour)
.style('cx', function(d) { return d.x} )
.style('cy', function(d) { return d.y} );
if(dataset.length > maxCircleCount) {
return true;
}
}
d3.timer(drawCircles, 1000);
})();
d3.timer usage explanation
# d3.timer(function[, delay[, time]])
[function] argument is called at every frame rendering by d3. It's called until it returns true.
optional [delay] in milliseconds to delay the first invocation of the [function]. Delay is taken since the [time] passed in third argument. If [time] is not passed delay starts from new Date().getTime().
optional [time] is the epoch time from when the delay is considered.
Reference https://github.com/mbostock/d3/wiki/Transitions#timers
I'm using d3 to animate a route (path) on a map. When the route reaches a point along the route I'd like to popup some information.
Most of my code is based on the following example. http://bl.ocks.org/mbostock/1705868. I'm really just trying to determine if there is a way to detect when the transitioning circle collides or overlaps any of the stationary circles in this example.
You can detect collision in your tween function. Define a collide function to be called from inside the tween function as follows:
function collide(node){
var trans = d3.transform(d3.select(node).attr("transform")).translate,
x1 = trans[0],
x2 = trans[0] + (+d3.select(node).attr("r")),
y1 = trans[1],
y2 = trans[1] + (+d3.select(node).attr("r"));
var colliding = false;
points.each(function(d,i){
var ntrans = d3.transform(d3.select(this).attr("transform")).translate,
nx1 = ntrans[0],
nx2 = ntrans[0] + (+d3.select(this).attr("r")),
ny1 = ntrans[1],
ny2 = ntrans[1] + (+d3.select(this).attr("r"));
if(!(x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1))
colliding=true;
})
return colliding;
}
Where points are the stationary points, and node is the transitioning element. What collide does is check whether node overlaps with any of the points (as shown in collision detection example here).
Because we need the node to be passed to the tween function, we replace attrTween used in Mike's example, with tween:
circle.transition()
.duration(10000)
.tween("attr", translateAlong(path.node()))
.each("end", transition);
Finally, the tween function calling our collide:
function translateAlong(path) {
var l = path.getTotalLength();
return function(d, i, a) {
return function(t) {
var p = path.getPointAtLength(t * l);
d3.select(this).attr("transform","translate(" + p.x + "," + p.y + ")");
if(collide(this))
d3.select(this).style("fill", "red")
else
d3.select(this).style("fill", "steelblue")
};
};
}
See the full demo here
The easiest way is to just check how "close" the transitioning circle is to the other points.
var pop = d3.select("body").append("div")
.style("position","absolute")
.style("top",0)
.style("left",0)
.style("display", "none")
.style("background", "yellow")
.style("border", "1px solid black");
// Returns an attrTween for translating along the specified path element.
function translateAlong(path) {
var l = path.getTotalLength();
var epsilon = 5;
return function(d, i, a) {
return function(t) {
var p = path.getPointAtLength(t * l);
points.forEach(function(d,i){
if ((Math.abs(d[0] - p.x) < epsilon) &&
(Math.abs(d[1] - p.y) < epsilon)){
pop.style("left",d[0]+"px")
.style("top",d[1]+20+"px")
.style("display","block")
.html(d);
return false;
}
})
return "translate(" + p.x + "," + p.y + ")";
};
};
}
The faster the circle moves the greater your epsilon will need to be.
Example here.
I started with the default Sunburst example and tried to add an animation on click - clicking on a wedge makes that wedge the center element. This works great except in a few cases - it looks like if the depth goes above 2 (0 based) then some wedges don't animate correctly; they seem to start the animation in the wrong place, but end up in the correct place.
Here is what I think is the relevant code, and I hope it's useful to someone else:
animate = function (d) {
var oldDs = [], topD, includeds;
if (animating)
return;
animating = true;
topD = (d == selected && d.parent) ? d.parent : d;
if (selected == topD) {
animating = false;
return;
}
selected = topD;
svg.selectAll("path").filter(function (f) {
return (!isDescendant(f, topD))
}).transition().duration(1000).style("opacity", "0").attr("pointer-events","none");
includeds = svg.datum(topD).selectAll("path").filter(function (f) {
return isDescendant(f, topD);
});
// step 1 - save the current arc settings
includeds.data().each(function (d) {
oldDs[d.ac_id] = {x: d.x, dx: d.dx, y: d.y, dy: d.dy};
});
// step 2 - recalculate arc settings and animate
includeds.data(partition.nodes) // recalculates
.transition().duration(1000).attrTween("d", function (d) {
var oldD = oldDs[d.ac_id],
interpolator = d3.interpolateObject(oldD, d);
return function (t) {
return arc(interpolator(t));
}
}).style("opacity", "1").attr("pointer-events","all");
setTimeout(function(){ animating = false; }, 1000);
};
Because my code doesn't have anything to do with the depth, I expect the issue is somewhere else, but I haven't been able to find it. The glitch seems to affect all browsers (at least Chrome, Firefox, and IE).
So, my question: what's the issue and how can I fix it?