I'm trying to implement drill-down capability in zoom function, i.e., I want that my initial plot shows, for example, 50 points, and when the user makes zoom the number of points increases to 500.
My attempt consists in redraw inside the zoom function all the points and remove part of them when the zoom scale is under a threshold. As you can see in this JSFIDDLE, the implementation reproduces the drill-down capability.
However, I suspect that there is a more efficient way to implement the drill-down. Therefore, the question is if I'm in the correct way or there is a standard (more efficient and elegant) way for doing this effect.
My example code:
var width = 300,
height = 300;
var randomX = d3.random.normal(width / 2, 40),
randomY = d3.random.normal(height / 2, 40);
var data = d3.range(500).map(function() {
return [randomX(), randomY()];
});
var svg = d3.select("body").append("svg");
var zoomBehav = d3.behavior.zoom();
svg.attr("height", height)
.attr("width", width)
.call(zoomBehav
.scaleExtent([1, 10])
.on("zoom", zoom));
// Initial plot
d3.select("svg").selectAll("circle")
.data(data, function(d,i) {return i;})
.enter()
.append("circle")
.attr("r", 3)
.attr("cx", function(d) {return d[0]; })
.attr("cy", function(d) {return d[1]; })
.style("fill", "red");
d3.selectAll("circle")
.filter(function(d, i) {
if (zoomBehav.scale() < 2) { return i > 50; }
})
.remove();
function zoom(){
var selection = d3.select("svg")
.selectAll("circle")
.data(data, function(d,i) { return i; });
selection
.attr("cx", function(d) { return d3.event.translate[0] + d3.event.scale * d[0]; })
.attr("cy", function(d) { return d3.event.translate[1] + d3.event.scale * d[1]; });
selection.enter()
.append("circle")
.attr("r", 3)
.attr("cx", function(d) { return d3.event.translate[0] + d3.event.scale * d[0]; })
.attr("cy", function(d) { return d3.event.translate[1] + d3.event.scale * d[1]; })
.style("fill", "red");
d3.selectAll("circle")
.filter(function(d, i) {
if (zoomBehav.scale() < 2) { return i > 50; }
})
.remove();
}
If you're interested in dealing with semantic zoom of elements on an XY canvas, then you'll want to look into d3.geom.quadtree:
https://github.com/mbostock/d3/wiki/Quadtree-Geom
You can pass your points to a quadtree and they'll be spatially nested. Then, you can tie the nesting level to the zoom level and have automatic grid clustering. It's rather more involved than would fit into this answer, since you have to come up with mechanisms for representing the clustered points, and you'll also need to get into recursive functions to deal with the hierarchical level of points.
Here's an example using quadtrees for semantic zoom and clustering for mapping:
http://bl.ocks.org/emeeks/066e20c1ce5008f884eb
Related
I am creating a horizontal bar chart using d3. And I am using an animation to "grow" the chart at startup. Here is the code.
// Create the svg element
d3.select("#chart-area")
.append("svg")
.attr("height", 800)
.attr("width", 800);
.data(dataValues) // This data is previously prepared
.enter().append("rect")
.style("fill", "blue")
.attr("x", function () { return xScale(0); }) // xScale is defined earlier
.attr("y", function (d) { return yScale(d); }) // yScale is defined earlier
.attr("height", yScale.bandwidth()) // yScale is defined earlier
// Initial value of "width" (before animation)
.attr("width", 0)
// Start of animation transition
.transition()
.duration(5000) // 5 seconds
.ease (d3.easeLinear);
// Final value of "width" (after animation)
.attr("width", function(d) { return Math.abs(xScale(d) - xScale(0)); })
The above code would work without any problem, and the lines would grow as intended, from 0 to whichever width, within 5 seconds.
Now, if we change the easing line to the following
// This line changed
.ease (d3.easeElasticIn);
Then, the ease would try to take the width to a negative value before going to a final positive value. As you can see here, d3.easeElasticIn returns negative values as time goes by, then back to positive, resulting in width being negative at certain points in the animation. So the bars do not render properly (because SVG specs state that if width is negative, then use 0)
I tried every solution to allow the bars to grow negatively then back out. But could not find any. How can I fix this problem?
Thanks.
As you already know, the use of d3.easeElasticIn in your specific code will create negative values for the rectangles' width, which is not allowed.
This basic demo reproduces the issue, the console (your browser's console, not the snippet's console) is populated with error messages, like this:
Error: Invalid negative value for attribute width="-85.90933910798789"
Have a look:
const svg = d3.select("svg");
const margin = 50;
const line = svg.append("line")
.attr("x1", margin)
.attr("x2", margin)
.attr("y1", 0)
.attr("y2", 150)
.style("stroke", "black")
const data = d3.range(10).map(function(d) {
return {
y: "bar" + d,
x: Math.random()
}
});
const yScale = d3.scaleBand()
.domain(data.map(function(d) {
return d.y
}))
.range([0, 150])
.padding(0.2);
const xScale = d3.scaleLinear()
.range([margin, 300]);
const bars = svg.selectAll(null)
.data(data)
.enter()
.append("rect")
.attr("x", margin)
.attr("width", 0)
.style("fill", "steelblue")
.attr("y", function(d) {
return yScale(d.y)
})
.attr("height", yScale.bandwidth())
.transition()
.duration(2000)
.ease(d3.easeElasticIn)
.attr("width", function(d) {
return xScale(d.x) - margin
})
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg></svg>
So, what's the solution?
One of them is catching those negative values as they are generated and, then, moving the rectangle to the left (using the x attribute) and converting those negative numbers to positive ones.
For that to work, we'll have to use attrTween instead of attr in the transition selection.
Like this:
.attrTween("width", function(d) {
return function(t){
return Math.abs(xScale(d.x) * t);
};
})
.attrTween("x", function(d) {
return function(t){
return xScale(d.x) * t < 0 ? margin + xScale(d.x) * t : margin;
};
})
In the snippet above, margin is just a margin that I created so you can see the bars going to the left of the axis.
And here is the demo:
const svg = d3.select("svg");
const margin = 100;
const line = svg.append("line")
.attr("x1", margin)
.attr("x2", margin)
.attr("y1", 0)
.attr("y2", 150)
.style("stroke", "black")
const data = d3.range(10).map(function(d) {
return {
y: "bar" + d,
x: Math.random()
}
});
const yScale = d3.scaleBand()
.domain(data.map(function(d) {
return d.y
}))
.range([0, 150])
.padding(0.2);
const xScale = d3.scaleLinear()
.range([0, 300 - margin]);
const bars = svg.selectAll(null)
.data(data)
.enter()
.append("rect")
.attr("x", margin)
.attr("width", 0)
.style("fill", "steelblue")
.attr("y", function(d) {
return yScale(d.y)
})
.attr("height", yScale.bandwidth())
.transition()
.duration(2000)
.ease(d3.easeElasticIn)
.attrTween("width", function(d) {
return function(t) {
return Math.abs(xScale(d.x) * t);
};
})
.attrTween("x", function(d) {
return function(t) {
return xScale(d.x) * t < 0 ? margin + xScale(d.x) * t : margin;
};
})
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg></svg>
I am new to d3.js and have been exploring how to visually make data more attractive since I have a background in visual communication.
I was wondering if any of you experts could give me a hand on solving a challenge I am facing with some animations.
I wanted to create an animate the graphs on the load of the page maybe with an elastic effect.
Here is what I have as an example for a bar chart with the transition I am imagining.
var bardata = [];
for (var i=0; i < 100; i++) {
bardata.push(Math.round(Math.random()*30))
}
var height = 400,
width = 600,
barWidth = 50,
barOffset = 5;
var tempColor;
var tooltip = d3.select('body').append('div')
.style('position', 'absolute')
.style('padding', '0 10px')
.style('background', 'white')
.style('opacity', 0)
var color = d3.scale.linear()
.domain([0, bardata.length*.33, bardata.length*.66, bardata.length])
.range(['#FFB832','#C61C6F', 'red', 'blue'])
var yScale = d3.scale.linear()
.domain([0, d3.max(bardata)])
.range([0, height]);
var xScale = d3.scale.ordinal()
.domain(d3.range(0, bardata.length))
.rangeBands([0, width])
var myChart = d3.select('#chart').append('svg')
.attr('width', width)
.attr('height', height)
.selectAll('rect').data(bardata)
.enter().append('rect')
.style('fill', function(d, i) {
return color(i);
})
.attr('width', xScale.rangeBand())
.attr('height', 0)
.attr('x', function(d,i) {
return xScale(i);
})
.attr('y', height - 0)
.on('mouseover', function(d) {
tooltip.transition()
.style('opacity', .9)
tooltip.html(d)
.style('left', (d3.event.pageX - 35) + 'px')
.style('top', (d3.event.pageY - 35) + 'px')
tempColor = this.style.fill;
d3.select(this)
.style('opacity', .5)
.style('fill', 'yellow')
})
.on('mouseout', function(d) {
tooltip.transition()
.style('opacity', 0)
d3.select(this)
.style('opacity', 1)
.style('fill', tempColor)
});
myChart.transition()
.attr('height', function(d) {
return yScale(d);
})
.attr('y', function(d) {
return height - yScale(d);
})
.delay(function(d,i) {
return i * 15;
})
.ease('elastic')
.duration(1000)
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<body>
<div class="container">
<h2>Bar chart</h2>
<div id="chart"></div>
</div>
</body>
Now I wanted to make a same transition for a donut chart, or better said an aster plot like this one below. Animation on load of page from center to outwards.
http://bl.ocks.org/bbest/2de0e25d4840c68f2db1
I have been trying again and again the last few weeks but since I am still new, I had to come here for some help.
Hope to here some expert advice on this!
Had the same issue (animating aster plot slices from inner radius to their final radius) and managed to get it working, merging stuff from the "Aster Plot" example and the "Arc Tween" example and adapting it to my data.
I don't know if it's the best solution (d3 noob also) but at least, it's working.
Basically, when drawing the arc, instead of calling directly the arc function, I put an attrTween calling an arcTween function that will call the arc function with interpolation.
So
arcs.append('path')
(other attr like filling, stroke, etc)
.attr('d',arc)
becomes
arcs.append("path")
// (other attr like filling, stroke, etc)
.transition()
.duration(500)
.attrTween('d', arcTween(innerRadius));
and the function arcTween is
function arcTween(newH) {
return function(d) {
var interpolate = d3.interpolate(newH, d.data.hauteur);
return function(t) {
d.data.hauteur = interpolate(t);
return arc(d);
}
}
}
d.data.hauteur being the outerRadius you want to get for that pie slice.
I have modified this sunburst diagram in D3 and would like to add text labels and some other effects. I have tried to adopt every example I could find but without luck. Looks like I'm not quite there yet with D3 :(
For labels, I would like to only use names of top/parent nodes and that they appear outside of the diagram (as per the image below). This doesn't quite work:
var label = svg.datum(root)
.selectAll("text")
.data(partition.nodes(root).slice(0,3)) // just top/parent nodes?
.enter().append("text")
.attr("class", "label")
.attr("x", 0) // middle of arc
.attr("dy", -10) // outside last children arcs
/*
.attr("transform", function(d) {
var angle = (d.x + d.dx / 2) * 180 / Math.PI - 90;
console.log(d, angle);
if (Math.floor(angle) == 119) {
console.log("Flip", d)
return ""
} else {
//return "scale(-1 -1)"
}
})
*/
.append("textPath")
.attr("xlink:href", function(d, i) { return "#path_" + i; })
.text(function(d) { return d.name + " X%"; });
I would also like to modify a whole tree branch on hover so that it 'shifts' outwards. How would I accomplish that?
function mouseover(d) {
d3.select(this) // current element and all its children
.transition()
.duration(250)
.style("fill", function(d) { return color((d.children ? d : d.parent).name); });
// shift arcs outwards
}
function mouseout(d) {
d3.selectAll("path")
.transition()
.duration(250)
.style("fill", "#fff");
// bring arcs back
}
Next, I'd like to add extra lines/ticks on the outside of the diagram that correspond to boundaries of top/parent nodes, highlighting them. Something along these lines:
var ticks = svg.datum(root).selectAll("line")
.data(partition.nodes) // just top/parent nodes?
.enter().append("svg:line")
.style("fill", "none")
.style("stroke", "#f00");
ticks
.transition()
.ease("elastic")
.duration(750)
.attr("x1", function(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x))); })
.attr("y1", function(d) { return Math.max(0, y(d.y + d.dy)); })
.attr("x2", function(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x))); })
.attr("y2", function(d) { return Math.max(0, y(d.y + d.dy) + radius/10); });
Finally, I would like to limit zoom level so the last nodes in the tree do not fire zoom but instead launch a URL (which will be added in JSON file). How would I modify the below?
function click(d) {
node = d;
path.transition()
.duration(750)
.attrTween("d", arcTweenZoom(d));
}
My full pen here.
Any help with this would be much appreciated.
I want to use D3 for generating charts from JSON files. How do I combine/relate bar chart and bubble chart so when you click on either it should provide you details of both charts in a legend.
The each bubble must be below and center of the each bar. It must share x-axis of bar charts.
There are two different data sources for them.
No. of bars = No. of bubbles
I created the xscale
var xScale = d3.scale.ordinal() .domain( d3.range(dataset.length))
.rangePoints([0, w-50]);
This same scale is shared while drawing bar as well as circles.
I have changed your codepen code
Below is the complete code after change
svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("x", function(d,i) {
//return i * (w / dataset.length);
return xScale(i) ;
})
.attr("y", function(d) {
return h - (d * 10);
})
.attr("width", barWidth)
.attr("height", function(d) {
return d * 10;
})
.attr("fill","#d38e71");
var circles = svg.selectAll("circle")
//.data(dataset)
.data(num)
.enter()
.append("a")
.attr("xlink:href", "http://google.com")
.attr("xlink:title",function(d){
return "No. of campagins sent - "+d;
})
.attr("target","_blank")
.append("circle");
circles.attr("cx", function(d,i) {
/* return i* (wB / dataset.length)+(wB / dataset.length)/2-2;*/
return (xScale(i) + barWidth/2);
})
.attr("cy", function(d) {
return hB-35;
})
.attr("r", function(d) {
return rScale(d);
})
Note the following code for bar
.attr("x", function(d,i) {
//return i * (w / dataset.length);
return xScale(i) ;
})
Note the following code for circles.
circles.attr("cx", function(d,i) {
/* return i* (wB / dataset.length)+(wB / dataset.length)/2-2;*/
return (xScale(i) + barWidth/2);
})
One suggestion
Width of the bar is set via this statement (w / dataset.length - barPadding) This can be stored in a variable. So it will not calculate for each member in the chart.
I am trying to make tooltip like: http://jsfiddle.net/6cJ5c/10/ for my graph and that is the result on my realtime graph: http://jsfiddle.net/QBDGB/52/ I am wondering why there is a gap between the circles and the graph and why at the beginning there is a vertical line of circles? When it starts the circles are close to the curve but suddendly they start to jump up and down !! I want the circles to move smooothly and stick on the surface of the curve. I think the problem is that they are not moving with the "path1" and so it does not recognize the circles and thats why they are moving separetly or maybe the value of tooltipis are different of the value of the curve so they do not overlap!. That is how the data is generated ( value and time) and the tooltip:
var data1 = initialise();
var data1s = data1;
function initialise() {
var arr = [];
for (var i = 0; i < n; i++) {
var obj = {
time: Date.now(),
value: Math.floor(Math.random() * 90)
};
arr.push(obj);
}
return arr;
}
// push a new element on to the given array
function updateData(a) {
var obj = {
time: Date.now(),
value: Math.floor(Math.random() * 90)
};
a.push(obj);
}
var formatTime = d3.time.format("%H:%M:%S");
//tooltip
var div = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
var blueCircles = svg.selectAll("dot")
.data(data1s)
.enter().append("circle")
.attr("r", 3)
.attr("cx", function(d) { return x(d.time); })
.attr("cy", function(d) { return y(d.value); })
.style("fill", "white")
.style("stroke", "red")
.style("stroke-width", "2px")
.on("mousemove", function(d ,i) {
div.transition()
.duration(650)
.style("opacity", .9);
div.html(formatTime(new Date(d.time)) + "<br/>" + d.value)
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
})
.on("mouseout", function(d ,i ) {
div.transition()
.duration(650)
.style("opacity", 0);
});
blueCircles.data(data1s)
.transition()
.duration(650)
.attr("cx", function(d) { return x(d.time); })
.attr("cy", function(d) { return y(d.value); });
Please kindly tell me your opinions since I really need it :(
As I said maybe I should add "mouseover and mouse move functions" to the "path" to make it recognize the tooltip. something like following. but I am nor really sure :(
var path1 = svg.append("g")
.attr("clip-path", "url(#clip)")
.append("path")
.data([data1])
.attr("class", "line1")
.on("mouseover", mouseover)
.on("mousemove", mousemove)
.on("mouseout", mouseout);
I think your problem lies in the interpolation of your paths. You set the interpolation between points on your var area to "basis", which I found is a B-spline interpolation. This means the area drawn does not go through the points in your dataset, as shown in this example:
The path your points move over, though, are just straight lines between the points in your dataset. I updated and changed the interpolation from basic to linear, to demonstrate that it will work that way. I also set the ease() for the movement to linear, which makes it less 'jumpy'. http://jsfiddle.net/QBDGB/53/