Scaling D3 chart inside svg while keeping margins the same width? - d3.js

Working on rendering charts using D3 that has to scale to different resolutions.
I would like to keep everything except the chart path itself at static size which includes all text, line thickness and margins.
I think I have figured out how to handle everything except the margins and cant find anyone who has the same issue so hopefully someone can help.
I've written a short test script that hopefully explains how I have my margins and everything else setup. The red are margins and the grey rectangle is where I draw my chart. Added a text element to show how I scale texts, line thickness etc.
What I want to achieve is that the margin have a static width as I scale the window. If I scale the svg and make it 100px wider I want the red rectangle to become 100px wider while the red margins stay the same.
I had been thinking about increasing the middle rectangle using the scale variable to have it increase "faster" but trying to use something like transform(scale on the rectangle in the resized() function produced unexpected results.
var margin = {top: 100, right: 150, bottom: 100, left: 150}
var outerWidth = 1600,
outerHeight = 900;
var width = outerWidth - margin.right - margin.left,
height = outerHeight - margin.top - margin.bottom;
svg = d3.select(".plot-div").append("svg")
.attr("class", "plot-svg")
.style("background-color", "red")
.attr("width", "100%")
.attr("viewBox", "0 0 " + outerWidth + " " + outerHeight);
g = svg.append("g")
.attr("class", "plot-space")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.append("rect")
.attr("width", width)
.attr("height", height)
.attr("fill", "grey");
text = d3.select(".plot-space").append("text")
.text("Text with the background")
.attr("y", 50)
.attr("x", 50)
.attr("font-size", 16)
.attr("font-family", "monospace")
.attr("fill", "white");
window.addEventListener("resize", resized);
function resized(){
var current_width = svg.node().getBoundingClientRect().width;
var scale = outerWidth / current_width;
text.attr("transform", "scale(" + scale + " " + scale + ")");
}
resized();
jsfiddle for you to play around with.
https://jsfiddle.net/2tuompye/4/

Related

D3 Pie Chart rotate only the arcs

My chart needs a label in the center of the donut that should not rotate when clicked.
https://codepen.io/scratchy303/pen/dyXMzrz
Can I append the label in a sibling "g"roup and rotate the arcs group?
Can I translate just the arcs rather than rotating the entire SVG?
What's the simplest solution?
var svg = d3.select("#pieChart").append("svg")
.attr("width", '100%')
.attr("height", '100%')
.attr('viewBox', '0 0 ' + Math.min(width, height) + ' ' + Math.min(width, height))
.attr('preserveAspectRatio', 'xMinYMin')
.append("g")
.attr("transform", "translate(" + radius + "," + height / 2 + ")")
.style("filter", "url(#drop-shadow)");
svg.append("g")
.append("text")
.attr("text-anchor", "middle")
.text("Donut Name");
I found a solution by counter-rotating my text. My biggest hurdle was simply traversing my SVG in D3.
//start by selecting the parent "g"roup and then you can select the text
d3.select(i.parentNode).select(".donut-label")
.transition()
.duration(1000)
.attr("transform", "rotate(" + (-angle) + ")");

d3js: Unable to place horizontally bars within the axis

I'm learning to make charts in d3 from scratch without taking someone else code and modifying it. I can successfully create a x & y axis vertical bar chart. But when it comes to transform the same chart to horizontal bar chart I end up in a mess. Here is my code so far:
var data = [{
name: "China",
value: 1330141295
}, {
name: "India",
value: 1173108018
}, {
name: "Indonesia",
value: 242968342
}, {
name: "Russia",
value: 139390205
}];
//set margins
var margin = {
top: 30,
right: 30,
bottom: 30,
left: 40
};
var width = 960 - margin.left - margin.right;
var height = 600 - margin.top - margin.bottom;
//set scales & ranges
var yScale = d3.scaleBand().range([0, height]).padding(0.1)
var xScale = d3.scaleLinear().range([0, width])
//draw the svg
var svg = d3.select("#chart").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom).append("g")
.attr("transform", "translate(" + margin.left * 2 + "," + margin.top + ")")
//load the data
data.forEach(function(d) {
d.population = +d.population;
});
//set domains
xScale.domain([0, d3.max(data, function(d) {
return d.population
})])
yScale.domain(data.map(d => d.name))
//add x & y Axis
svg.append("g")
.attr("transform", "translate(" + 0 + "," + height + ")")
.call(d3.axisBottom(xScale));
svg.append("g")
.call(d3.axisLeft(yScale))
.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("y", d => yScale(d.name))
.attr("height", d => yScale(d.name))
.attr("x", d => width - xScale(d.population))
.attr("width", yScale.bandwidth())
Thank you very much.
You need to change a lot of things in your code.
TL;DR
change value to population in the array
scales are used to convert values to proportional pixel values
height is the vertical size of an element. you should use yScale(d.name)
width is the horizontal size of an element. you should use xScale(d.population)
y is the vertical position of an element. you should use yScale.bandwidth()
x is the vertical position of an element. you should use 0
use selectAll("rect") on a new appended g or the svg element not the same g element that has the axises on it
add fill attribute so that your rects have color
You have the population field labelled value but you're calling population through out the code to use it. So replace value with population in your data objects.
Next you need to change the way you're setting up the rects. use selectAll on the svg element directly or append another g to the svg element and add the rects on that element. Right now your code attempts to add the rects to the same g element as the x axis.
Make sure you are setting the attributes of the rects correctly. Height is the size in pixels of the rect element not the position. y is the position of the rects vertically from the top downwards. this means the height attribute should use yScale(d.name) and width should use xScale(d.population) because they are the width and length of the rectangles, or rect elements. x and y are the coordinate positions of the element from the top left corner of the svg element. Use them to determine the starting position of the top left pixel of your rects. You should have y as yScale.bandwidth() and x as 0.

D3 clipping issues on zoom

I would like to use zoom behaviour in a simple d3 graph. The thing is that whenever I zoom in, the "main" group elements take up the whole svg space, which causes other existing elements such as axes and texts to get overlapped. I have read that clipping can be used to solve this problem but I didn't manage to get it working properly.
The following image (zoom in was applied) shows the problem:
Related example with what I have tried so far can be found here.
I was finally able to solve this problem. Key points were:
Use proper svg element for clipping, normally rect does the job with corresponding width/height (same as your "drawing" area).
Transform all elements drawn within the clip path (region), and not the parent group.
In code (omitting irrelevant parts), the result is:
// Scales, axis, etc.
...
// Zoom behaviour & event handler
let zoomed = function () {
let e = d3.event;
let tx = Math.min(0, Math.max(e.translate[0], width - width*e.scale));
let ty = Math.min(0, Math.max(e.translate[1], height - height*e.scale));
zoom.translate([tx,ty]);
main.selectAll('.circle').attr('transform', 'translate(' + [tx,ty] + ')scale(' + e.scale + ')');
svg.select('.x.axis').call(xAxis);
svg.select('.y.axis').call(yAxis);
}
let zoom = d3.behavior.zoom()
.x(x)
.y(y)
.scaleExtent([1,8])
.on('zoom', zoomed);
const svg = d3.select('body').append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.attr('pointer-events', 'all')
.call(zoom);
const g = svg.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
// Set clip region, rect with same width/height as "drawing" area, where we will be able to zoom in
g.append('defs')
.append('clipPath')
.attr('id', 'clip')
.append('rect')
.attr('x', 0)
.attr('y', 0)
.attr('width', width)
.attr('height', height);
const main = g.append('g')
.attr('class', 'main')
.attr('clip-path', 'url(#clip)');
let circles = main.selectAll('.circle').data(data).enter();

How can I split an ordinal axis in d3?

The following is my draw axis code:
var seasons = ["summer", "winter", "fall", "spring"];
var margin = {top:80, right:30, bottom:30, left:30},
width = 1200 - margin.right - margin.left,
height = 800 - margin.top - margin.bottom;
var x = d3.scale.ordinal()
.domain(seasons)
.rangeRoundBands([0, width], 0.9);
xAxis = d3.svg.axis()
.scale(x)
.tickSize(4, 6)
.tickPadding(6)
.orient("bottom");
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
</script>
However, the tickPadding function does now introduce a space between the ordinal axis categories.
More specifically, I want that each of the summer, winter, fall and spring parts of the axis are separate from each other, sort of like dashed line. How can I get this?
I don't know of any way built into the d3 axis to accomplish this, but you can remove the path it draws and replace it with a dashed line, like so:
// Draw the axis, as you currently are
var axisElem = svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
// Remove the line
axisElem.selectAll("path.domain").remove();
// Figure out how big each dash should be
var gapFraction = 0.1; // The portion of the line that should be a gap
var total = x(seasons[1]) - x(seasons[0]);
var dash = total * (1 - gapFraction);
var gap = total * gapFraction;
// Draw the dashed line
axisElem.append("line")
.classed("domain", true)
.attr("x1", x(seasons[0]) - dash / 2 + gap / 2)
.attr("x2", x(seasons[seasons.length - 1]) + dash / 2 - gap / 2)
.attr("y1", 0)
.attr("y2", 0)
.attr("stroke", "black")
.attr("stroke-dasharray", dash + "," + gap);

make a circle progress bar, but animate arc.endAngle (angle >180 degree) stops working in d3.js

I am running D3.js to draw a progress bar in circle shape, which you will see the demo on jsfiddle , the progress bar has a transition animation.
The main code is
var width = 960,
height = 500,
twoPi = 2 * Math.PI,
progress = 0,
total = 1308573, // must be hard-coded if server doesn't report Content-Length
formatPercent = d3.format(".0%");
var arc = d3.svg.arc()
.startAngle(0)
.innerRadius(0)
.outerRadius(240);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var meter = svg.append("g")
.attr("class", "progress-meter");
meter.append("path")
.attr("class", "background")
.attr("d", arc.endAngle(twoPi));
var foreground = meter.append("path")
.attr("class", "foreground");
foreground.attr("d", arc.endAngle(twoPi * 0))
foreground.transition().duration(1500).attr("d", arc.endAngle( twoPi * 2/3 ));
var text = meter.append("text")
.attr("text-anchor", "middle")
.attr("dy", ".35em");
to make the progress bar move, we only need to change to the arc.endAngle(), which is on the line.
foreground.transition().duration(1500).attr("d", arc.endAngle( twoPi * 2/3 ));
if the angle is less than 180, ( endangle < twoPi*1/2), then the animation works fine, but when the angle is larger than 180, so means endangle >= twoPi*1/2. then the animation would not show, and if you look at the console, you will find many errors on d3.js
Error: Problem parsing d="M1.1633760361312584e-14,-190A190,190 0 1.481481481481482e-7,1 -0.000022772330200401806,-189.9999883969182L0,0Z" meeting.html:1
2
Error: Problem parsing d="M1.1633760361312584e-14,-190A190,190 0 2.56e-7,1 -0.00003935058659476369,-189.99997994987467L0,0Z"
so what is the exact problem for this, how to solve it
It doesn't work because you can't use the standard transition for radial paths. By default, it simply interpolates the numbers without knowing what they represent, so in your case, you end up with some really small numbers (e.g. 1.1633760361312584e-14) which Javascript represents in exponential notation which is not valid for SVG paths.
The solution is to use a custom tween function that knows how to interpolate arcs:
function arcTween() {
var i = d3.interpolate(0, twoPi * 2/3);
return function(t) {
return arc.endAngle(i(t))();
};
}
Complete example here. You may also be interested in this example, which shows how to do it with data bound to the paths.

Resources