Expanding D3 Pie Chart to fill SVG - d3.js

I am trying to achieve two things:
Make the existing pie chart in my application fill the available SVG
element it is rendered in.
Make the SVG element fill the size of the containing div it sits in
so it is responsive.
In my bar charts I achieve this by setting ScaleLinear and ScaleBand ranges on the X and Y scale but this doesn't seem to be an option within the pie charts (and then setting the SVG element to a height and width of 100%).
Code:
export default Component.extend({
tagName: 'svg',
attributeBindings: ['width, height'],
classNameBindings: ['baseClass'],
a: null,
baseClass: 'pie-chart',
color: null,
data: null,
labelArc: null,
height: 400,
radius: null,
svg: null,
width: 400,
donutwidth: 75,
setSvg() {
const {
height,
baseClass,
width,
} = this.getProperties(
'height',
'baseClass',
'width'
);
const svg = select(`.${baseClass}`)
.attr('width', width)
.attr('height', height)
.append('g')
.attr('transform', `translate(${width/2}, ${height/2})`);
this.set('svg', svg);
},
_setG(svg, p) {
return svg.selectAll('arc')
.data(p)
.enter()
.append('g')
.attr('class', 'arc');
},
_setPie(data) {
const p = pie().padAngle(0.02).value((d) => d.count)(data);
return p;
},
// Template
<svg width='100%' height='100%'></svg>
Any help is gratefully appreciated

I found i needed to set the initial margins to pad the SVG, then calculate max and min values for pie elements using the available svgWidth data
var margin = {top: 20, right: 100, bottom: 30, left: 40};
var svgWidth = window.innerWidth - (window.innerWidth/4);
var width = svgWidth,
height = (Math.min(width) / 2) + 100,
radius = Math.min(width, height) / 3;
var oRadius = radius, //var holding value for the outer radius of the arc
iRadius = Math.min(width, height) / 4, //var holding the value for the inner radius of the arc
cRadius = 8; //var holding the value for the corner radius of the arc
var piePad = 5;
///////////////////////////////////////////////////////////////
////// Graphing Function
///////////////////////////////////////////////////////////////
function graph(_selection) {
_selection.each(function(data) {
var pie = d3.pie()
.padAngle(.01)
.sort(null)
.value(function(d) {
// console.log(d.value.value)
return d.value.value;
});
///////////////////////////////////////////////////////////////
////// Scales
///////////////////////////////////////////////////////////////
var max = d3.max(data, function(d) { return d.value; });
var min = d3.min(data, function(d) { return d.value; });
var colorScale = setColorScale(max);
///////////////////////////////////////////////////////////////
////// Pie Vars
///////////////////////////////////////////////////////////////
var pie = d3.pie()
.padAngle(.01)
.value(function(d) {return d.value;})
// .value(function(d) { return d[1]; })
.sort(null);
var arc = d3.arc()
.padRadius(oRadius + piePad)
.outerRadius(oRadius - piePad)
// .innerRadius(radius - (radius/2.piePad));
.innerRadius(iRadius);
// .cornerRadius(cRadius);
var outerArc = d3.arc()
.padRadius(oRadius + piePad)
.innerRadius(radius * 0.9)
.outerRadius(radius * 0.9);
// .cornerRadius(cRadius);
var arcOut = d3.arc()
.padRadius(oRadius + piePad)
.innerRadius(iRadius - piePad*4)
.outerRadius(oRadius - piePad);
// .cornerRadius(cRadius);
var arcOver = d3.arc()
.padRadius(oRadius + piePad)
.innerRadius(iRadius - piePad*2)
.outerRadius(oRadius - piePad*2);
///////////////////////////////////////////////////////////////
////// Build Initial SVG
///////////////////////////////////////////////////////////////
if (!svg){
svg = d3.select(this).append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("class", "donut-group")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
}
///////////////////////////////////////////////////////////////
////// Add paths for pie
///////////////////////////////////////////////////////////////
var path = svg.selectAll("path").data(pie(data));
path.enter()
.append("path")
.attr("class",animatePathIn)
/// rest of path code
} // end graph
return graph;
}

Related

axis scale shift in d3.js

I am fairly new to d3.js
I am looking for a way to animate both x and y axises based on the new data. So it is more of a real time animation where the x axis is moving and the new data pops out from the right and y axis get updated dynamically as well and after a while the old data dissapear because I have so many data points.
I have this chart already made. https://jsfiddle.net/elvalencian/mfLjovx9/4/
// set the dimensions and margins of the graph
const margin = {
top: 40,
right: 80,
bottom: 60,
left: 50
},
width = 600 - margin.left - margin.right,
height = 300 - margin.top - margin.bottom;
// append the svg object to the body of the page
const svg = d3
.select("#root")
.append("svg")
.attr(
"viewBox",
`0 0 ${width + margin.left + margin.right} ${
height + margin.top + margin.bottom}`)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
//Read the data
d3.csv("https://raw.githubusercontent.com/sultanmalki/d3js/main/saudi_fdi.csv",
// When reading the csv, I must format variables:
function(d) {
return {
date: d3.timeParse("%Y")(d.date),
value: d.value
}
},
// Now I can use this dataset:
function(data) {
// Add X axis --> it is a date format
var x = d3.scaleTime()
.domain(d3.extent(data, function(d) {
return d.date;
}))
.range([0, width]);
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.attr("class", "axis")
.transition().duration(5000)
.call(d3.axisBottom(x));
// Add Y axis
var y = d3.scaleLinear()
.domain([0, d3.max(data, function(d) {
return +d.value;
})])
.range([height, 0]);
svg.append("g")
.attr("class", "axis")
.transition()
.ease(d3.easeLinear)
.duration(5000)
.call(d3.axisLeft(y));
// Add the line
const linePath = svg
.append("path")
.datum(data)
.attr("fill", "none")
.attr("stroke", "#00B0F1")
.attr("stroke-width", 1.5)
.attr("d", d3.line().curve(d3.curveCardinal)
.x(function(d) {
return x(d.date)
})
.y(function(d) {
return y(d.value)
})
)
const pathLength = linePath.node().getTotalLength();
linePath
.attr("stroke-dasharray", pathLength)
.attr("stroke-dashoffset", pathLength)
.attr("stroke-width", 3)
.transition()
.attr("transform", "translate(" + ")")
.duration(5000)
.attr("stroke-width", 3)
.attr("stroke-dashoffset", 0);
})
I would really appreciate any help.
thank you in advance
Lines are rather difficult to animate, since they are only one path object instead of multiple objects as e. g. in a bar or scatter plot. You are already using the stroke-dasharray attribute for the animation of the static data. When there is new data, you need to
Rescale the axes:
To achieve this, compute the domain for x and y and set it via the domain method. Then re-render the axes with call(AxisObject) using a transition. Use the same transition t for both x and y.
Rescale the existing line
With the rescaled axes, also the existing line path must be rescaled. This works smoothly by transitioning the d attribute using the transition t before binding the new data.
Add new data
Wait till the end of transition t to bind the new data to the line path. Before doing that, calculate getTotalLength in order to set stroke-dasharray such that the new data is initially hidden. Then transition stroke-dasharray to the new path length. As the second value for stroke-dasharray I used 9999 which must be chosen longer than the maximal expected path length of the new data.
// Some stuff to generate random time series
// Standard Normal variate using Box-Muller transform.
function randn() {
let u = 0, v = 0;
while (u === 0) u = Math.random();
while (v === 0) v = Math.random();
return Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
}
// Simulate geometric brownian motion
const mu = 0.8;
const sigma = 0.5;
function simulate() {
const prev = data[data.length - 1];
const x = prev.x + 0.01;
const bm = prev.bm + Math.sqrt(0.01) * randn();
data.push({
x: x,
bm: bm,
y: Math.exp((mu - sigma * sigma / 2) * x + sigma * bm)
});
}
// Initial data
let data = [{
x: 0,
bm: 0,
y: 1,
}];
// Add data to chart in chunks
const blockSize = 20;
let blockCounter = 0;
function addData() {
simulate();
blockCounter += 1;
if (blockCounter === blockSize) {
render(data.slice());
blockCounter = 0;
}
}
// Chart definitions
const width = 500,
height = 180,
marginLeft = 30,
marginRight = 10,
marginBottom = 30,
marginTop = 10;
const svg = d3.select("svg")
.attr("width", width)
.attr("height", height);
const xSlidingWindow = 2;
const x = d3.scaleLinear()
.range([marginLeft, width - marginRight]);
const y = d3.scaleLinear()
.range([height - marginBottom, marginTop]);
const xAxis = d3.axisBottom(x);
const yAxis = d3.axisLeft(y).ticks(3);
const line = d3.line()
.x(d => x(d.x))
.y(d => y(d.y));
const gx = svg.append("g")
.attr("transform", `translate(0,${height - marginBottom})`);
const gy = svg.append("g")
.attr("transform", `translate(${marginLeft},0)`);
// Clip path to only show lines inside the axes
const clipPath = svg.append("clipPath")
.attr("id", "clip-rect")
.append("rect")
.attr("x", marginLeft)
.attr("y", marginTop)
.attr("width", width - marginLeft - marginRight)
.attr("height", height - marginTop - marginBottom);
const path = svg.append("path")
.datum(data.slice())
.attr("clip-path", "url(#clip-rect)")
.attr("fill", "none")
.attr("stroke", "blue")
.attr("stroke-width", 2)
.attr("stroke-dasharray", "0, 9999");
function render(arr) {
// compute domain
const xMax = d3.max(arr, d => d.x);
x.domain([Math.max(xMax - xSlidingWindow, 0), Math.max(xSlidingWindow, xMax)]);
y.domain(d3.extent(arr, d => d.y));
// First, transition the axes
const t = d3.transition().duration(interval * blockSize / 2);
gx.transition(t).call(xAxis);
gy.transition(t).call(yAxis);
path.transition(t).attr("d", line);
t.on("end", () => {
// Then add new data
let pathLength = path.node().getTotalLength();
path.datum(arr)
.attr("stroke-dasharray", `${pathLength}, 9999`)
.attr("d", line);
pathLength = path.node().getTotalLength();
path.transition().duration(interval * blockSize / 2)
.attr("stroke-dasharray", `${pathLength}, 9999`)
.attr("d", line);
});
}
// Interval for data simulation
let intervalId;
const interval = 50;
function startStream() {
if (!intervalId) {
intervalId = setInterval(addData, interval);
}
}
function stopStream() {
clearInterval(intervalId);
intervalId = null;
}
function reset() {
clearInterval(intervalId);
data = [{
x: 0,
bm: 0,
y: 1,
}];
intervalId = setInterval(addData, interval);
}
d3.select("#start").on("click", startStream);
d3.select("#stop").on("click", stopStream);
d3.select("#reset").on("click", reset);
render(data.slice());
startStream();
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.3.0/d3.min.js"></script>
<div>
<button id="start">Start</button>
<button id="stop">Stop</button>
<button id="reset">Reset</button>
</div>
<svg></svg>

How to rotate SVG using d3.drag()?

I have a simple rectangle appended as a SVG. I want to rotate it with a mouse mouse drag so I used the function d3.drag(). Here is what I have attempted in order to achieve this but it does not seem to work:
<div id = "svgcontainer"></div>
<script language = "javascript">
var width = 300;
var height = 300;
var origin = {
x: 55,
y: -40
};
var svg = d3.select("#svgcontainer")
.append("svg")
.attr("width", width)
.attr("height", height);
var group = svg.append("g");
var rect = group.append("rect")
.attr("x", 20)
.attr("y", 20)
.attr("width", 60)
.attr("height", 30)
.attr("fill", "green")
group.call(d3.drag().on('drag', dragged));
function dragged() {
var r = {
x: d3.event.x,
y: d3.event.y
};
group.rotate([origin.x + r.x, origin.y + r.y]);
};
</script>
When I click on the rectangle and try to drag it to rotate, I am getting some error in the last line with group.rotate(...). Can anyone please sort out the mistake in this code.
group is a d3 selection holding a g, it doesn't have a rotate method, but you can set the transform attribute for the selection with:
group.attr("transform",rotate(θ,cx,cy));
From the example, I'm unsure on how you want to rotate the block, I've set in in the example below to rotate around the center based on the movement of the drag along the x axis:
var width = 300;
var height = 300;
var origin = {
x: 50,
y: 35
};
var svg = d3.select("#svgcontainer")
.append("svg")
.attr("width", width)
.attr("height", height);
var group = svg.append("g");
var rect = group.append("rect")
.attr("x", 20)
.attr("y", 20)
.attr("width", 60)
.attr("height", 30)
.attr("fill", "green")
group.call(d3.drag().on('drag', dragged));
function dragged() {
var r = {
x: d3.event.x,
y: d3.event.y
};
group.attr("transform","rotate("+r.x+","+origin.x+","+origin.y+")" );
};
rect {
cursor: pointer;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
<div id="svgcontainer"></div>

How to use D3 zoom behavior with ViewBox instead of transform

I would like to take advantage of D3's zoom behavior functionality, but I need to do all translations/scaling of my SVG using the viewBox property instead of the transform method as shown in the D3 example: http://bl.ocks.org/mbostock/3680999
How can I achieve this same scale/translate using only the viewBox? Here's my code so far, which doesn't work well like the transform method.
function zoomed(d) {
if (!scope.drawLine) {
var scale = d3.event.scale;
var translation = d3.event.translate;
//This works, but I can't use it for reason's I won't go into now
//mapSVG_G.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
var newViewBox = [
initialViewBox[0] - translation[0],
initialViewBox[1] - translation[1],
initialViewBox[2]/scale,
initialViewBox[3]/scale
];
mapSVG.attr('viewBox', newViewBox);
}
}
a bit off, but could serve you as a start:
main piece:
var newViewBox = [
-translate[0] / scale,
-translate[1] / scale,
width / scale,
height / scale
].join(" ");
whole example:
var width = 960,
height = 500;
var randomX = d3.random.normal(width / 2, 80),
randomY = d3.random.normal(height / 2, 80);
var data = d3.range(2000).map(function() {
return [
randomX(),
randomY()
];
});
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height].join(" "))
var vis = svg.append("g")
.call(d3.behavior.zoom().scaleExtent([1, 8]).on("zoom", zoom))
.append("g");
vis.append("rect")
.attr("class", "overlay")
.attr("width", width)
.attr("height", height);
vis.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("r", 2.5)
.attr("transform", function(d) {
return "translate(" + d + ")";
});
function zoom() {
var scale = d3.event.scale;
var translate = d3.event.translate;
var newViewBox = [
-translate[0] / scale,
-translate[1] / scale,
width / scale,
height / scale
].join(" ");
svg.attr('viewBox', newViewBox);
}
.overlay {
fill: none;
pointer-events: all;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

Making different size of `radius` to create `arc` not working

I am creating pie chart using d3.js. I would like to create 3 pies with single svg element with animation.
This is working fine for me. But do creating different I am reducing the radius each time using a loop. But the radius not getting changed.
How to solve this?
my code (sample) :
var array1 = [
0,200
]
window.onload = function () {
var width = 660,
height = 200,
radius = Math.min(width, height) / 2;
var color = d3.scale.category20();
var arc = null;
var pie = d3.layout.pie()
.value(function(d) {
return d; })
.sort(null);
function tweenPie(finish) {
var start = {
startAngle: 0,
endAngle: 0
};
var i = d3.interpolate(start, finish);
return function(d) { return arc(i(d)); };
}
var svg1 = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
for( var i = 0; i < 3; i++) {
arc = d3.svg.arc()
.innerRadius(radius - (5*i)) //each time size differs
.outerRadius(radius - (6)*i); //each time size differs
svg1.append('g')
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")")
.datum(array1).selectAll("path")
.data(pie)
.enter().append("path")
.attr("fill", function(d, i) { return color(i); })
.transition()
.duration(5000)
.attrTween('d', tweenPie)
}
}
Live Demo
There is a single arc variable that is being used in the tweenPie method and in the for loop. Each time through the for loop, the arc variable is set to a new value. The tweenPie method is called for each pie chart after the for loop exits. As a result, all the pie charts are using the same tweenPie method which is using the arc created in the last for loop.
For each pie chart, you need to create a separate tweenPie method with its own arc. For example...
var array1 = [ 0, 200 ]
window.onload = function () {
var width = 660,
height = 200,
radius = Math.min(width, height) / 2;
var color = d3.scale.category20();
var arc = null;
var pie = d3.layout.pie()
.value(function(d) {
return d; })
.sort(null);
function getTweenPie(arc) {
return function (finish) {
var start = {
startAngle: 0,
endAngle: 0
};
var i = d3.interpolate(start, finish);
return function(d) { return arc(i(d)); };
}
}
var svg1 = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
for( var i = 0; i < 3; i++) {
arc = d3.svg.arc()
.innerRadius(radius - (5*i)) //each time size differs
.outerRadius(radius - (6)*i); //each time size differs
svg1.append('g')
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")")
.datum(array1).selectAll("path")
.data(pie)
.enter().append("path")
.attr("fill", function(d, i) { return color(i); })
.transition()
.duration(5000)
.attrTween('d', getTweenPie(arc))
}
}

D3.js Bar graph not displaying properly?

I have a bar graph in my program, but it doesn't seem to be displaying properly. All of the bars seem to be a lot bigger than they are supposed to be. Here's the relevant code:
//Bar Graph
var canvas = d3.select("#canvas");
canvas.width = 500;
canvas.height = 500;
var values = [1, 2, 3]
var colours = ['#FA0', '#0AF', '#AF0']
var data = []
var yOffset = 0
//create scale
yRange2 = d3.scale.linear().range([canvas.height - MARGINS.top,
MARGINS.bottom]).domain([0, 6]);
//Process the data
for (var i = 0; i < values.length; i++) {
var datum = {
value: yRange2(values[i]),
colour: colours[i],
x: 0,
y: yOffset
}
yOffset += datum.value;
data.push(datum)
}
//setup y
yAxis2 = d3.svg.axis()
.scale(yRange2)
.tickSize(5)
.orient("left")
.tickSubdivide(true);
canvas.append("svg:g")
.attr("class", "y axis")
.attr("transform", "translate(" + (MARGINS.left) + ",0)")
.call(yAxis2);
var bars = canvas.selectAll('rect').data(data)
bars
.enter()
.append('rect')
.attr({
width: 30,
x: 60,
y: function (d) {
return d.y
},
height: function (d) {
return d.value;
}
})
.style({
fill: function (d) {
return d.colour
}
})
//updates when slider changes
$("#myRange").change(function () {
slider = $("#myRange").val();
updateXs();
updateLineData();
displayVals();
d3.select(".myLine").transition()
.attr("d", lineFunc(lineData));
});
And here's the full code:
http://jsfiddle.net/tqj5maza/7/
To me, it looks like the bars are starting at the top for some reason, and then going downwards, hence the cutoff. The height for each seems too large, though.
You aren't setting the rects height property correctly. Generally this is height of the plotting area minus y position. The way you have your code structured its:
height: function (d) {
return canvas.height - MARGINS.top - d.value;
}
To fix the overlapping x value, you should set up an x d3.scale but a quick and dirty way would be:
x: function(d,i){
return (i + 1) * 60; //<-- move each bar over 60 pixels
}
Updated code here.

Resources