I have drawn a spiral using this code:
var width = 1000,
height = 1000,
start = 0,
end = 2.25,
numSpirals = 78,
margin = {top:50,bottom:50,left:50,right:50};
// Constructing the spiral:
// theta for the spiral
var theta = function(r) {
return numSpirals * Math.PI * r;
};
// the r works out the space within which the spiral can take shape - the width and height is set above
var r = d3.min([width, height]) / 2 - 40 ;
// The radius of the spiral
var radius = d3.scaleLinear()
.domain([start, end])
.range([40, r]);
// inserts svg into the DOM
var svg = d3.select("#chart").append("svg")
.attr("width", width + margin.right + margin.left)
.attr("height", height + margin.left + margin.right)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
// The path to draw the spiral needs data to inform it, points generates this, and is used in .datum(points) below
var points = d3.range(start, end + 0.02, (end - start) / 2000);
// this is the spiral, utilising the theta and radius generated above
var spiral = d3.radialLine()
.curve(d3.curveCardinal)
.angle(theta)
.radius(radius);
var path = svg.append("path")
.datum(points)
.attr("id", "spiral")
.attr("d", spiral)
.style("fill", "none")
.style("stroke", "grey")
.style("stroke", ("6, 5"))
.style("opacity",0.5);
But, now I want to draw some lines on top of this spiral, following the existing path of the spiral. These lines will need to connect two different dates across two different columns within the array. I have tried a few approaches, such as d3.line but I can't get the lines to follow the spiral. I imagine I somehow need to reference the initial spiral? I am unsure how to proceed with this though.
Related
The snippet below creates a single x axis with starting ticks of 10. During zoom I'm updating ticks on the rescaled axis with:
.ticks(startTicks * Math.floor(event.transform.k))
With .scaleExtent([1, 50]) I can get down from years to 3-hourly blocks fairly smoothly (besides a little label overlap here and there).
But, when I request the number of ticks applied on the scale (xScale.ticks().length) I get a different number to the one I just assigned.
Also, when I get the labels (xScale.ticks().map(xScale.tickFormat())) they differ from the ones rendered as I get deeper into the zoom.
Reading here:
An optional count argument requests more or fewer ticks. The number of
ticks returned, however, is not necessarily equal to the requested
count. Ticks are restricted to nicely-rounded values (multiples of 1,
2, 5 and powers of 10), and the scale’s domain can not always be
subdivided in exactly count such intervals. See d3.ticks for more
details.
I understand I might not get the number of ticks I request, but it's counter-intuitive that:
I request more and more ticks (per k) - between 10 and 500
Then the returned ticks fluctuates between 5 and 19.
Why is this ? Is there a better or 'standard' way to update ticks whilst zooming for scaleTime or scaleUtc ?
var margin = {top: 0, right: 25, bottom: 20, left: 25}
var width = 600 - margin.left - margin.right;
var height = 40 - margin.top - margin.bottom;
// x domain
var x = d3.timeDays(new Date(2020, 00, 01), new Date(2025, 00, 01));
// start with 10 ticks
var startTicks = 10;
// zoom function
var zoom = d3.zoom()
.on("zoom", (event) => {
var t = event.transform;
xScale
.domain(t.rescaleX(xScale2).domain())
.range([0, width].map(d => t.applyX(d)));
var zoomedRangeWidth = xScale.range()[1] - xScale.range()[0];
var zrw = zoomedRangeWidth.toFixed(4);
var kAppliedToWidth = kw = t.k * width;
var kw = kAppliedToWidth.toFixed(4);
var zoomTicks = zt = startTicks * Math.floor(t.k);
svg.select(".x-axis")
.call(d3.axisBottom(xScale)
.ticks(zt)
);
var realTicks = rt = xScale.ticks().length;
console.log(`zrw: ${zrw}, kw: ${kw}, zt: ${zt}, rt: ${rt}`);
console.log(`labels: ${xScale.ticks().map(xScale.tickFormat())}`);
})
.scaleExtent([1, 50]);
// x scale
var xScale = d3.scaleTime()
.domain(d3.extent(x))
.range([0, width]);
// x scale copy
var xScale2 = xScale.copy();
// svg
var svg = d3.select("#scale")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.call(zoom)
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
// clippath
svg.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("x", 0)
.attr("width", width)
.attr("height", height);
// x-axis
svg.append("g")
.attr("class", "x-axis")
.attr("clip-path", "url(#clip)")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(xScale)
.ticks(startTicks));
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.3.1/d3.min.js"></script>
<div id="scale"></div>
The issue is in how the xScale is being updated on zoom.
The current approach in the example is:
xScale
.domain(t.rescaleX(xScale2).domain())
.range([0, width].map(d => t.applyX(d)));
This is doing two things:
Creating a rescaled copy of xScale2, but only to get its domain.
Extending the range of the xScale depending on the transform.
Because of step 2, the scale range is growing outside of the screen. When you request 500 ticks but only see 10, it is because there are 490 out of the viewport.
The solution is that continuous scales don't need to have the range updated on zoom, because the rescaleX method is enough for the transformation process.
The appropriate way to rescale a continuous scale on zoom is:
xScale = t.rescaleX(xScale2)
Which changes only the domain and keeps the range intact.
Consider this example to illustrate why only changing the domain is enough: If a scale maps from a domain [0,1] to a range [0, 100], and it is transformed with rescaleX, the new scale will now map from another domain (say, [0.4, 0.6]) to the same range [0, 100]. This is the zoom concept: it was showing data from 0 to 1 in a 100 width viewport, but now it is showing data from 0.4 to 0.6 in the same viewport; it "zoomed in" to 0.4 and 0.6.
The incorrect format returned from xScale.tickFormat() was a consequence of the range extension, but also of a mismatch between the displayed ticks and the computed ticks. The method only return the same ticks that are displayed if it also consideres the same amount of ticks, which is informed in the first parameter (in your example, it would be xScale.tickFormat(zt)). Since it had no arguments, it defaults to 10, and the 10 ticks computed in the time scale could be different or be in a different time granularity than the zt ticks that are displayed.
In summary, the snippet needs three changes:
Change 1: Update only the domain directly with rescaleX.
Change 2: Fix zoom ticks to a number, such as 10.
Change 3: Consider the number of ticks when using the tickFormat method.
The snippet below is updated with those changes:
var margin = {top: 0, right: 25, bottom: 20, left: 25}
var width = 600 - margin.left - margin.right;
var height = 40 - margin.top - margin.bottom;
// x domain
var x = d3.timeDays(new Date(2020, 00, 01), new Date(2025, 00, 01));
// start with 10 ticks
var startTicks = 10;
// zoom function
var zoom = d3.zoom()
.on("zoom", (event) => {
var t = event.transform;
// Change 1: Update only the domain directly with rescaleX
xScale = t.rescaleX(xScale2);
var zoomedRangeWidth = xScale.range()[1] - xScale.range()[0];
var zrw = zoomedRangeWidth.toFixed(4);
var kAppliedToWidth = kw = t.k * width;
var kw = kAppliedToWidth.toFixed(4);
// Change 2: Fix zoom ticks to a number, such as 10
var zoomTicks = zt = 10
svg.select(".x-axis")
.call(d3.axisBottom(xScale)
.ticks(zt)
);
var realTicks = rt = xScale.ticks().length;
console.log(`zrw: ${zrw}, kw: ${kw}, zt: ${zt}, rt: ${rt}`);
// Change 3: Consider zt when using the tickFormat method
console.log(`labels: ${xScale.ticks().map(xScale.tickFormat(zt))}`);
})
.scaleExtent([1, 50]);
// x scale
var xScale = d3.scaleTime()
.domain(d3.extent(x))
.range([0, width]);
// x scale copy
var xScale2 = xScale.copy();
// svg
var svg = d3.select("#scale")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.call(zoom)
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
// clippath
svg.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("x", 0)
.attr("width", width)
.attr("height", height);
// x-axis
svg.append("g")
.attr("class", "x-axis")
.attr("clip-path", "url(#clip)")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(xScale)
.ticks(startTicks));
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.3.1/d3.min.js"></script>
<div id="scale"></div>
I have a .json file with data, and I'd like to make a d3 donut (pie) chart from it. I'm not especially fluent in javascript, and every example I can find either pulls from inline json data or the json file is structured differently than mine (mine is a list of dictionaries; theirs are often single dictionaries). I've been troubleshooting for a few days, and somehow can't land on anything that actually works. Any thoughts/tips?
The example at https://www.d3-graph-gallery.com/graph/donut_label.html uses inline json data to render a donut chart with labels. I've attempted to modify it that code by:
pulling json data from /data/all-facet-digitized.json
pull labels each dictionary's "facet" key ("true" and "false"), and values from each dictionary's "count" key (373977 and 55433).
change the color scale domain to match the facet keys ("true" and "false")
/data/all-facet-digitized.json looks like:
[
{
"count": "55433",
"facet": "true"
},
{
"count": "373977",
"facet": "false"
}
]
Code in the of my html file looks like:
<div id="chart"></div> <!-- div containing the donut chart -->
<script src="//d3js.org/d3.v4.min.js"></script>
<script>
// set the dimensions and margins of the graph
var width = 450
height = 450
margin = 40
// The radius of the pieplot is half the width or half the height (smallest one) minus margin.
var radius = Math.min(width, height) / 2 - margin
// append the svg object to the div called 'chart'
var svg = d3.select("#chart")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
// Parse the Data
d3.json("/data/all-facet-digitized.json", function(data) {
// set the color scale
var color = d3.scaleOrdinal()
.domain(["true","false"])
.range(d3.schemeDark2);
// Compute the position of each group on the pie:
var pie = d3.pie()
.sort(null) // Do not sort group by size
.value(function(d) {return d.count; })
var data_ready = pie(d3.entries(data))
// The arc generator
var arc = d3.arc()
.innerRadius(radius * 0.5) // This is the size of the donut hole
.outerRadius(radius * 0.8)
// Another arc that won't be drawn. Just for labels positioning
var outerArc = d3.arc()
.innerRadius(radius * 0.9)
.outerRadius(radius * 0.9)
// Build the pie chart: Basically, each part of the pie is a path that we build using the arc function.
svg
.selectAll('allSlices')
.data(data_ready)
.enter()
.append('path')
.attr('d', arc)
.attr('fill', function(d){ return(color(d.facet)) })
.attr("stroke", "white")
.style("stroke-width", "2px")
.style("opacity", 0.7)
// Add the polylines between chart and labels:
svg
.selectAll('allPolylines')
.data(data_ready)
.enter()
.append('polyline')
.attr("stroke", "black")
.style("fill", "none")
.attr("stroke-width", 1)
.attr('points', function(d) {
var posA = arc.centroid(d) // line insertion in the slice
var posB = outerArc.centroid(d) // line break: we use the other arc generator that has been built only for that
var posC = outerArc.centroid(d); // Label position = almost the same as posB
var midangle = d.startAngle + (d.endAngle - d.startAngle) / 2 // we need the angle to see if the X position will be at the extreme right or extreme left
posC[0] = radius * 0.95 * (midangle < Math.PI ? 1 : -1); // multiply by 1 or -1 to put it on the right or on the left
return [posA, posB, posC]
})
// Add the polylines between chart and labels:
svg
.selectAll('allLabels')
.data(data_ready)
.enter()
.append('text')
.text( function(d) { console.log(d.facet) ; return d.facet} )
.attr('transform', function(d) {
var pos = outerArc.centroid(d);
var midangle = d.startAngle + (d.endAngle - d.startAngle) / 2
pos[0] = radius * 0.99 * (midangle < Math.PI ? 1 : -1);
return 'translate(' + pos + ')';
})
.style('text-anchor', function(d) {
var midangle = d.startAngle + (d.endAngle - d.startAngle) / 2
return (midangle < Math.PI ? 'start' : 'end')
})
})
</script>
My result renders as an empty space:
<div id="chart">
<svg width="450" height="450">
<g transform="translate(225,225)"></g>
</svg>
</div>
The schemeDark2 doens't exist in d3 v4. I've replaced it with schemeCategory10:
var color = d3.scaleOrdinal()
.domain(["true","false"])
.range(d3.schemeCategory10);
Since you have an array of objects, you don't need d3.entries. That takes an object and converts it to an array where each key is an item of the array. But since you already have an array here, you can put it directly in pie():
// Compute the position of each group on the pie:
var pie = d3.pie()
.sort(null) // Do not sort group by size
.value(function(d) {return d.count; })
var data_ready = pie(data)
Now that you've got the data, you can access it on any of the functions: try putting console.log(data_ready) to see what's available. You'll see that the data is bound for each object as the .data property. pie() takes an array and puts it in a format that's convenient to make pie charts with.
Say we want to access the facet property: we would access that as item.data.facet. So in your functions, to access, you can do:
svg
.selectAll('allSlices')
.data(data_ready)
.enter()
.append('path')
.attr('d', arc)
.attr('fill', function(d){ return(color(d.data.facet)) })
<head></head>
<div id="chart"></div> <!-- div containing the donut chart -->
<script src="//d3js.org/d3.v4.min.js"></script>
<script>
// set the dimensions and margins of the graph
var width = 450
height = 450
margin = 40
// The radius of the pieplot is half the width or half the height (smallest one) minus margin.
var radius = Math.min(width, height) / 2 - margin
// append the svg object to the div called 'chart'
var svg = d3.select("#chart")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
// Parse the Data
var data = [
{
"count": "55433",
"facet": "true"
},
{
"count": "373977",
"facet": "false"
}
]
// set the color scale
var color = d3.scaleOrdinal()
.domain(["true","false"])
.range(d3.schemeCategory10);
// Compute the position of each group on the pie:
var pie = d3.pie()
.sort(null) // Do not sort group by size
.value(function(d) {return d.count; })
var data_ready = pie(data)
console.log('data_r', data_ready)
// The arc generator
var arc = d3.arc()
.innerRadius(radius * 0.5) // This is the size of the donut hole
.outerRadius(radius * 0.8)
// Another arc that won't be drawn. Just for labels positioning
var outerArc = d3.arc()
.innerRadius(radius * 0.9)
.outerRadius(radius * 0.9)
// Build the pie chart: Basically, each part of the pie is a path that we build using the arc function.
svg
.selectAll('allSlices')
.data(data_ready)
.enter()
.append('path')
.attr('d', arc)
.attr('fill', function(d){ return(color(d.data.facet)) })
.attr("stroke", "white")
.style("stroke-width", "2px")
.style("opacity", 0.7)
// Add the polylines between chart and labels:
svg
.selectAll('allPolylines')
.data(data_ready)
.enter()
.append('polyline')
.attr("stroke", "black")
.style("fill", "none")
.attr("stroke-width", 1)
.attr('points', function(d) {
var posA = arc.centroid(d) // line insertion in the slice
var posB = outerArc.centroid(d) // line break: we use the other arc generator that has been built only for that
var posC = outerArc.centroid(d); // Label position = almost the same as posB
var midangle = d.startAngle + (d.endAngle - d.startAngle) / 2 // we need the angle to see if the X position will be at the extreme right or extreme left
posC[0] = radius * 0.95 * (midangle < Math.PI ? 1 : -1); // multiply by 1 or -1 to put it on the right or on the left
return [posA, posB, posC]
})
// Add the polylines between chart and labels:
svg
.selectAll('allLabels')
.data(data_ready)
.enter()
.append('text')
.text( function(d) { return d.data.facet} )
.attr('transform', function(d) {
var pos = outerArc.centroid(d);
var midangle = d.startAngle + (d.endAngle - d.startAngle) / 2
pos[0] = radius * 0.99 * (midangle < Math.PI ? 1 : -1);
return 'translate(' + pos + ')';
})
.style('text-anchor', function(d) {
var midangle = d.startAngle + (d.endAngle - d.startAngle) / 2
return (midangle < Math.PI ? 'start' : 'end')
})
</script>
Ok, the issues here is that you've completely missed how data_ready is structured after converting the JSON response. You might want to add console.log(data_ready) just after you set data_ready and inspect it in the console for better understanding of the following fixes.
First a color fix:
.attr('fill', function(d){ return(color(d.data.value.facet)) })
Then a data fix:
.value(function(d) {return d.value.count; })
And lastly a label fix:
.text( function(d) { console.log(d.data.key) ; return d.data.value.facet } )
Your script should look like this:
// set the dimensions and margins of the graph
var width = 450
height = 450
margin = 40
// The radius of the pieplot is half the width or half the height (smallest one). I subtract a bit of margin.
var radius = Math.min(width, height) / 2 - margin
// append the svg object to the div called 'my_dataviz'
var svg = d3.select("#chart")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
d3.json("/data/all-facet-digitized.json", function(data) {
// set the color scale
var color = d3.scaleOrdinal()
.domain(["true","false"])
.range(d3.schemeDark2);
// Compute the position of each group on the pie:
var pie = d3.pie()
.sort(null) // Do not sort group by size
.value(function(d) {return d.value.count; })
var data_ready = pie(d3.entries(data))
// The arc generator
var arc = d3.arc()
.innerRadius(radius * 0.5) // This is the size of the donut hole
.outerRadius(radius * 0.8)
// Another arc that won't be drawn. Just for labels positioning
var outerArc = d3.arc()
.innerRadius(radius * 0.9)
.outerRadius(radius * 0.9)
// Build the pie chart: Basically, each part of the pie is a path that we build using the arc function.
svg
.selectAll('allSlices')
.data(data_ready)
.enter()
.append('path')
.attr('d', arc)
.attr('fill', function(d){ return(color(d.data.value.facet)) })
.attr("stroke", "white")
.style("stroke-width", "2px")
.style("opacity", 0.7)
// Add the polylines between chart and labels:
svg
.selectAll('allPolylines')
.data(data_ready)
.enter()
.append('polyline')
.attr("stroke", "black")
.style("fill", "none")
.attr("stroke-width", 1)
.attr('points', function(d) {
var posA = arc.centroid(d) // line insertion in the slice
var posB = outerArc.centroid(d) // line break: we use the other arc generator that has been built only for that
var posC = outerArc.centroid(d); // Label position = almost the same as posB
var midangle = d.startAngle + (d.endAngle - d.startAngle) / 2 // we need the angle to see if the X position will be at the extreme right or extreme left
posC[0] = radius * 0.95 * (midangle < Math.PI ? 1 : -1); // multiply by 1 or -1 to put it on the right or on the left
return [posA, posB, posC]
})
// Add the polylines between chart and labels:
svg
.selectAll('allLabels')
.data(data_ready)
.enter()
.append('text')
.text( function(d) { console.log(d.data.key) ; return d.data.value.facet } )
.attr('transform', function(d) {
var pos = outerArc.centroid(d);
var midangle = d.startAngle + (d.endAngle - d.startAngle) / 2
pos[0] = radius * 0.99 * (midangle < Math.PI ? 1 : -1);
return 'translate(' + pos + ')';
})
.style('text-anchor', function(d) {
var midangle = d.startAngle + (d.endAngle - d.startAngle) / 2
return (midangle < Math.PI ? 'start' : 'end')
})
})
Given a set of arcs that make up a circle and random points generated inside of said circle, what's the best way to generate an area or chord that connects a slice of the array to one of the points and not just the exact center?
I was thinking that a ribbon or chord layout would be helpful here but the chord layout seems specific to connecting arcs (though admittedly I've only spent about two days researching it and am struggling with actual usage)
Right now I have a simple arbitrary arc and circle as such -
var width = 1000;
var height = 600;
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width/2 + "," + height/2 + ")");
let arc = svg.append("path")
.datum({
id: 1,
startAngle: 0,
endAngle: .50 * (2 * Math.PI)
})
.style("fill", "blue")
.attr("d", d3.arc()
.innerRadius(180)
.outerRadius(200))
let circle = svg.append("circle")
.attr("cx", 0)
.attr("cy", 0)
.attr("gradientUnits", "objectBoundingBox")
.attr("r", 20);
Simple fiddle - http://jsfiddle.net/968o4s9m/
Seems the best way is to draw a path manually by entering all points using lines and arcs. Ribbons do not appear to be able to connect arbitrary points and arcs from what I can tell.
Here is some example code and a fiddle of it:
var w = 400;
var h = 400;
var r = 20;
var factor = 5;
var svg = d3.select("body").append("svg")
.attr("width", w)
.attr("height", h)
.append("g")
.attr("transform", "translate(" + w/2 + "," + h/2 + ")");
svg.append("circle")
.attr("cx", 0)
.attr("cy", 0)
.attr("r", r)
.style("fill", "black");
svg.append("circle")
.attr("cx", 150)
.attr("cy", 150)
.attr("r", r)
.style("fill", "red");
svg.selectAll("circle")
.transition()
.duration(2000)
.attr("transform", "scale(" + 1/factor +")")
.attr("r", r*factor);
http://jsfiddle.net/o1wzfas7/2/
In the example, I am scaling two circles down by a factor of 5 (which also scales their positions and thus moves them "closer" to each other) and simultaneously scaling up the circles' radii by a factor of 5. The idea is that they'll appear to move closer to each other without changing size (as if I was changing their "cx" and "cy" attributes instead), but for some reason the scale transition and radius transition seem to go at different rates, so you see the circles get larger and then settle back to the initial size.
Does anybody know how I would do this using scale and radius transitions, but having the two cancel each other out so that the circles don't appear to change in size?
First, to explain what's going on:
The problem is that the changes you are making cancel out multiplicatively, but transitions proceed in an additive way.
So for your simple example, where radius (r) starts at 20, the scale (s) starts out (implicitly) as 1 and you are transitioning by a factor of 5, the effective radius of the circle is r*s:
At the start of transition:
r =20
s =1
r*s =20
At the end of transition:
r =4
s =5
r*s =20
Now, the way you're thinking of it in your head is that the factor should transition from 1 to 5, but that's not what is going to happen. The default transition functions don't see your factor, they just see that radius is transitioning from 20 to 4, and scale is transitioning from 1 to 5.
Therefore, at the midpoint of the transition, each attribute will be at the midpoint (average) of its start and end values:
r = (20+4)/2 = 12
s = (1+5)/2 = 3
r*s = 36
In order to do what you want, you're going to have to create a custom tween, which directly transitions the factor, and then calculates the radius and scale from there:
svg.selectAll("circle")
.transition()
.duration(2000)
.tween("factor", function(d,i){
/* create an interpolator for your factor */
var f = d3.interpolateNumber(1,factor);
/* store the selected element in a variable for easy modification */
var c = d3.select(this);
/* return the function which will do the updates at each tick */
return function(t) {
var f_t = f(t);
c.attr("transform", "scale(" + 1/f_t + ")" );
c.attr("r", r*f_t );
};
});
Note that in your real application, you'll need to store the "start" value for your factor transition in a global variable or each data object, since it won't automatically be 1 when you transition to a different scaling factor.
var w = 400;
var h = 400;
var r = 20;
var factor = 5;
var svg = d3.select("body").append("svg")
.attr("width", w)
.attr("height", h)
.append("g")
.attr("transform", "translate(" + w/2 + "," + h/2 + ")");
svg.append("circle")
.attr("cx", 0)
.attr("cy", 0)
.attr("r", r)
.style("fill", "black");
svg.append("circle")
.attr("cx", 150)
.attr("cy", 150)
.attr("r", r)
.style("fill", "red");
svg.selectAll("circle")
.transition()
.duration(2000)
.tween("factor", function(d,i){
/* create an interpolator for your factor */
var f = d3.interpolateNumber(1,factor);
/* store the selected element in a variable for easy modification */
var c = d3.select(this);
/* return the function which will do the updates at each tick */
return function(t) {
var f_t = f(t);
c.attr("transform", "scale(" + 1/f_t + ")" );
c.attr("r", r*f_t );
};
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
I have several graphs set up to zoom on the container and it works great. However, on the initial load, the zoom level is way too close. Is there a method of setting the initial zoom level to avoid having to first zoom out? I am familiar with the .scale() method but have not had any luck implementing it. Is this the way to go or is there something I am missing?
Here is what I have thus far as pertaining to zoom:
var margin = {top: 20, right: 120, bottom: 20, left: 120},
width = 50000 - margin.right - margin.left,
height = 120000 - margin.top - margin.bottom;
var x = d3.scale.linear()
.domain([0, width])
.range([0, width]);
var y = d3.scale.linear()
.domain([0, height])
.range([height, 0]);
var tree = d3.layout.tree()
.size([height, width])
.separation(function(a, b) { return (a.parent == b.parent ? 1 : 2) / a.depth; });
var diagonal = d3.svg.diagonal()
.projection(function(d) { return [d.x, d.y]; });
function zoom(d) {
svg.attr("transform",
"translate(" + d3.event.translate + ")"+ " scale(" + d3.event.scale + ")");
}
var svg = d3.select("body").append("svg")
.attr("width", width + margin.right + margin.left)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.attr("pointer-events", "all")
.call(d3.behavior.zoom()
.x(x)
.y(y)
.scaleExtent([0,8])
.on("zoom", zoom))
.append('g');
svg.append('rect')
.attr('width', width*5)
.attr('height', height)
.attr('border-radius', '20')
.attr('fill', 'sienna');
D3v4 answer
If you are here looking for the same but with D3 v4,
var zoom = d3.zoom().on("zoom", function(){
svg.attr("transform", d3.event.transform);
});
vis = svg.append("svg:svg")
.attr("width", width)
.attr("height", height)
.call(zoom) // here
.call(zoom.transform, d3.zoomIdentity.translate(100, 50).scale(0.5))
.append("svg:g")
.attr("transform","translate(100,50) scale(.5,.5)");
I finally got this to work by setting both the initial transform and the zoom behavior to the same value.
var zoom = d3.behavior.zoom().translate([100,50]).scale(.5);
vis = svg.append("svg:svg")
.attr("width", width)
.attr("height", height)
.call(zoom.on("zoom",zooming))
.append("svg:g")
.attr("transform","translate(100,50)scale(.5,.5)");
Applies to d3.js v4
This is similar to davcs86's answer, but it reuses an initial transform and implements the zoom function.
// Initial transform to apply
var transform = d3.zoomIdentity.translate(200, 0).scale(1);
var zoom = d3.zoom().on("zoom", handleZoom);
var svg = d3.select("body")
.append('svg')
.attr('width', 800)
.attr('height', 300)
.style("background", "red")
.call(zoom) // Adds zoom functionality
.call(zoom.transform, transform); // Calls/inits handleZoom
var zoomable = svg
.append("g")
.attr("class", "zoomable")
.attr("transform", transform); // Applies initial transform
var circles = zoomable.append('circle')
.attr("id", "circles")
.attr("cx", 100)
.attr("cy", 100)
.attr('r', 20);
function handleZoom(){
if (zoomable) {
zoomable.attr("transform", d3.event.transform);
}
};
See it in action: jsbin link
Adding this answer as an addendum to the accepted answer in case anyone is still having issues:
The thing that made this really easy to understand was looking here
That being said, I set three variables:
scale, zoomWidth and zoomHeight
scale is the initial scale you want the zoom to be, and then
zoomWidth and zoomHeight are defined as follows:
zoomWidth = (width-scale*width)/2
zoomHeight = (height-scale*height)/2
where width and height are the width and height of the "vis" svg element
the translate above is then amended to be:
.attr("transform", "translate("+zoomWidth+","+zoomHeight+") scale("+scale+")")
as well as the zoom function:
d3.behavior.zoom().translate([zoomWidth,zoomHeight]).scale(scale)
What this does is effectively ensures that your element is zoomed and centered when your visualization is loaded.
Let me know if this helps you! Cheers.
D3JS 6 answer
Let's say that you want your initial position and scale to be x, y, scale respectively.
const zoom = d3.zoom();
const svg = d3.select("#containerId")
.append("svg")
.attr("width", width)
.attr("height", height)
.call(zoom.transform, d3.zoomIdentity.translate(x, y).scale(scale)
.call(zoom.on('zoom', (event) => {
svg.attr('transform', event.transform);
}))
.append("g")
.attr('transform', `translate(${x}, ${y})scale(${k})`);
.call(zoom.transform, d3.zoomIdentity.translate(x, y).scale(scale) makes sure that when the zoom event is fired, the event.transform variable takes into account the translation and the scale. The line right after it handles the zoom while the last one is used to apply the translation and the scale only once on "startup".
I was using d3 with react and was very frustrated about the initial zoom not working.
I tried the solutions here and none of them worked, what worked instead was using an initial scale factor and positions and then updating the zoom function on the basis of those scale factor and positions
const initialScale = 3;
const initialTranslate = [
width * (1 - initialScale) / 2,
height * (1 - initialScale) / 2,
];
const container = svg
.append('g')
.attr(
'transform',
`translate(${initialTranslate[0]}, ${initialTranslate[1]})scale(${initialScale})`
);
The zoom function would look something like this
svg.call(
zoom().on('zoom', () => {
const transformation = getEvent().transform;
let {x, y, k} = transformation;
x += initialTranslate[0];
y += initialTranslate[1];
k *= initialScale;
container.attr('transform', `translate(${x}, ${y})scale(${k})`);
})
);
If you noticed the getEvent() as a function, it was because importing event from d3-selection was not working in my case. So I had to do
const getEvent = () => require('d3-selection').event;