D3: Rescaling scatter plot elements with Update, without removing elements - d3.js

I have some rookie question.
I am trying to rescale scatter plot elements (circles along with X axis) based on button click - to create 'zoom in' activity. I am lowering X axis domain for that.
It works very well with Xaxis, however its harder with circles. I must remove all of them and draw them again (the place marked in the code). This makes the process slow when lot of elements involved and also doesn't allow transition which is applied for axis scaling.
Is it possible just to change attributes of circles that i won't need to remove and redraw everything again?
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<script src="https://d3js.org/d3.v4.min.js"></script>
</head>
<body>
<script>
// Define Canvas size
var width = 500;
var height = 500;
// Generate Toy Data
var randomX = d3.randomUniform(0, 880);
var randomY = d3.randomUniform(0, 800);
data_x = [];
data_y = [];
for (i = 0; i < 10000; i++) {
nume = Math.floor((Math.random() * 100));
data_x.push(nume);
data_y.push(nume);
}
// Scaling Coeffs
var coeffX = 1;
var coeffY = 1;
var coeffR = 1;
var coordX = (Math.max.apply(null, data_x)/coeffX);
var coordy = (Math.max.apply(null, data_y)/coeffY);
var x = d3.scaleLinear()
.domain([0, coordX])
.range([ 0, width]).nice();
var y = d3.scaleLinear()
.domain([0, coordy])
.range([height, 0]).nice();
var yAxis = d3.axisLeft(y).ticks(5);
var xAxis = d3.axisBottom(x).ticks(5)
//Create SVG element
var canvas = d3.select("body")
.append("svg")
.attr("width", width+50)
.attr("height", height+50)
.append("g");
// --------------- Draw Elements ---------------------
var circles = canvas.selectAll("circlepoint")
.data(data_x)
.enter()
.append("g");
circles.append("circle")
.attr("cx",function(d, i){
var tempe = data_x[i];
return x(tempe);
})
.attr("cy", function(d, i){
var tempe = data_y[i];
return y(tempe);
})
.attr("r", function(){
return 3;
})
.style("stroke", "steelblue")
.style("fill", "none")
.attr("transform", "translate(40, -20)");
var xsCont = canvas.append("g")
.attr("transform", "translate(40, " + (height-20) +")")
.attr("class","xaxis")
.call(xAxis);
var ysCont = canvas.append("g")
.attr("transform", "translate(40, -20)")
.call(yAxis);
function rescaleAxisX(tempCoeffX, tempCoeffR){
coordX = (Math.max.apply(null, data_x)/tempCoeffX);
x.domain([0, Math.max.apply(null, data_x)/tempCoeffX])
// -------- This part works well-------
canvas.select(".xaxis")
.transition().duration(750)
.call(xAxis);
// -------- End -------------
// -------- This one doesn't as expected-------
circles.remove();
circles = canvas.selectAll("circlepoint")
.data(data_x)
.enter()
.append("g");
circles.append("circle")
.attr("cx",function(d, i){
var tempe = data_x[i];
return x(tempe);
})
.attr("cy", function(d, i){
var tempe = data_y[i];
return y(tempe);
})
.attr("r", function(d, i){
return tempCoeffR;
})
.style("stroke", "steelblue")
.style("fill", "none")
.attr("transform", "translate(40, -20)");
}
// -------- End -------
// Zoom in button
var buttonZoomPlusX = d3.select("body")
.append("input")
.attr("type","button")
.attr("class","button")
.attr("value", "+")
.on("click", function(){
coeffX = coeffX + 1;
coeffR = coeffR + 1;
rescaleAxisX(coeffX, coeffR);
})
</script>
</body>
</html>
Here is implemented Fiddle
https://jsfiddle.net/sma76ntq/
Thanks a lot in advance

Well it was rookie mistake as always. Two things were missing:
1. Appending some class to circles in order not too select everything on canvas if there were more circles drawn
circles.append("circle")
.attr("cx",function(d, i){
var tempe = data_x[i];
return x(tempe);
})
.attr("cy", function(d, i){
var tempe = data_y[i];
return y(tempe);
//return y(d.y);
})
.attr("r", function(){
return 3;
})
.attr("class","eles")
.style("stroke", "steelblue")
.style("fill", "none")
.attr("transform", "translate(40, -20)");
on redrawn just select classes and change attributes.
canvas.selectAll(".eles").data(data_x)
.transition()
.duration(750)
.attr("cx",function(d, i){
var tempe = data_x[i];
return x(tempe);
})
.attr("cy", function(d, i){
var tempe = data_y[i];
return y(tempe);
});
Working Fiddle here: https://jsfiddle.net/v7q14nbm/1/

Related

How do I get rid of tiny lines between canvas rects

I am very new to D3 and as you can see in the image above there are tiny lines/gaps between each rectangle that I would love to get rid of, this is drawn on a canvas element with each rectangle starting where the last one ends using D3.js following this tutorial almost exactly minus adding the gaps between each square.
I've tried
this.canvas.imageSmoothingQuality = 'low';
draw() {
const canvas = d3
.select(this.chartContainer.nativeElement)
.append('canvas')
.attr('width', this.width)
.attr('height', this.height)
.attr(
'transform',
'translate(' + this.margin.left + ',' + this.margin.top + ')'
);
this.canvas = canvas.node().getContext('2d');
this.clearCanvas();
this.canvas.imageSmoothingQuality = 'low';
const elements = this.shadowContainer.selectAll('custom.rect');
const _this = this;
elements.each(function(d, i) {
const node = d3.select(this);
// Here you retrieve the colour from the individual in-memory node and set the fillStyle for the canvas paint
_this.canvas.fillStyle = node.attr('color');
// Here you retrieve the position of the node and apply it to the fillRect context function which will fill and paint the square.
_this.canvas.fillRect(
Number(node.attr('x')),
Number(node.attr('y')),
Number(node.attr('width')),
Number(node.attr('height'))
);
});
}
private dataBind(value) {
const customBase = document.createElement('custom');
this.shadowContainer = d3.select(customBase);
const {
viewModes: {
heatMap: {
data,
chartOptions: { engagementStatus, xAxis, yAxis }
}
}
} = value;
const x = this.d3
.scaleBand()
.range([0, this.width])
.domain(xAxis.categories);
this.shadowContainer
.append('g')
.style('font-size', 11)
.attr('class', 'x-axis')
.call(this.d3.axisTop(x).tickSize(0))
.select('.domain')
.remove();
this.shadowContainer
.selectAll('.x-axis text')
.style('text-anchor', 'start')
.attr('transform', function(d) {
return `translate(8, -8)rotate(-90)`;
});
const y = this.d3
.scaleBand()
.domain(d3.reverse(yAxis.categories))
.range([this.height, 0]);
const color = this.d3
.scaleLinear()
.domain([-2, -1, 0, 1])
// #ts-ignore
.range(['#5b717d', '#ffb957', '#ee6b56', '#40a050']);
const join = this.shadowContainer
.selectAll('custom.rect')
.data(data, function(d) {
return `${d.Date}:${d.Member}`;
});
const enterSelection = join
.enter()
.append('custom')
.attr('class', 'rect')
.attr('x', d =>
this.getCorrectDatePosition(
d.Date,
x,
xAxis.categories[0].split('/').length
)
)
.attr('y', function(d) {
return y(d.Member);
})
.attr('width', 24)
.attr('height', 24);
join
.merge(enterSelection)
.attr('width', x.bandwidth())
.attr('height', y.bandwidth())
.attr('color', function(d) {
return color(d.score);
});
const exitSelection = join
.exit()
.transition()
.attr('width', 0)
.attr('height', 0)
.remove();
}
This is likely an issue stemming from your scales. It can occur with either SVG or canvas and occurs when dealing with coordinates that require plotting at fractions of a pixel.
Here's a demonstration with SVG:
var data = d3.range(20);
var x = d3.scaleBand()
.range([10,250])
.domain(data)
var svg = d3.select("body")
.append("svg")
.attr("width", 500);
var rect = svg.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("x", d=>x(d) )
.attr("y", 50)
.attr("width", x.bandwidth())
.attr("height",100)
.attr("fill","crimson")
svg.transition()
.attrTween("tween", function() {
var i = d3.interpolate(250,480)
return function(t) {
x.range([50,i(t)])
rect.attr("x",d=>x(d))
.attr("width", x.bandwidth());
return "";
}
})
.duration(10000);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
And one with Canvas:
var data = d3.range(20);
var x = d3.scaleBand()
.range([10,250])
.domain(data)
var canvas = d3.select("body")
.append("canvas")
.attr("width", 500);
var rect = d3.create("div").selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("x", d=>x(d) )
.attr("y", 50)
.attr("width", x.bandwidth())
.attr("height",100)
.attr("fill","crimson")
canvas.transition()
.attrTween("tween", function() {
var i = d3.interpolate(250,480)
var context = canvas.node().getContext("2d");
return function(t) {
x.range([50,i(t)])
context.fillStyle = "#fff";
context.fillRect(0,0,550,300);
rect.attr("x",d=>x(d))
.attr("width", x.bandwidth())
.each(function() {
var node = d3.select(this);
context.fillStyle = "crimson"
context.fillRect(
+node.attr("x"),
+node.attr("y"),
+node.attr("width"),
+node.attr("height"))
})
return "";
}
})
.duration(10000);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
The solution is to be a bit more involved in setting the scale's domain and range. Start with the desired bandwidth, a whole number in pixels, and set the range so that the difference between the minimum and maximum values is equal to the number of values in the domain * the bandwidth.
So instead of:
const x = this.d3
.scaleBand()
.range([0, this.width])
.domain(xAxis.categories);
You'd have:
const length = 10; // length of a box side
const x = this.d3
.scaleBand()
.domain(xAxis.categories)
.range([0,xAxis.categories * length])
You could also calculate length above dynamically, say by using: Math.floor(width/xAxis.categories)
Using the above approach and a slightly contrived example to accommodate the transition, we remove the aliasing/moire pattern. Because we use only full pixels, the transition jumps as each bar increases in width by a full pixel at the same time, as space becomes available in the range:
var data = d3.range(20);
var length = 30;
var x = d3.scaleBand()
.range([10,data.length*length])
.domain(data)
var canvas = d3.select("body")
.append("canvas")
.attr("width", 500);
var rect = d3.create("div").selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("x", d=>x(d) )
.attr("y", 50)
.attr("width", x.bandwidth())
.attr("height",100)
.attr("fill","crimson")
canvas.transition()
.attrTween("tween", function() {
var i = d3.interpolate(250,480)
var context = canvas.node().getContext("2d");
return function(t) {
length = Math.floor(i(t)/data.length)
x.range([10,length*data.length+10])
context.fillStyle = "#fff";
context.fillRect(0,0,550,300);
rect.attr("x",d=>x(d))
.attr("width", x.bandwidth())
.each(function(d,i) {
var node = d3.select(this);
context.fillStyle = d3.schemeCategory10[i%10];
context.fillRect(
+node.attr("x"),
+node.attr("y"),
+node.attr("width"),
+node.attr("height"))
})
return "";
}
})
.duration(10000);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

Changing the angle of a D3 doughnut chart to 180

I need to make my doughnut chart a horizontal graph like in this image >
this is the code that i use for other doughnut charts
var dataset = {
hddrives: [total - value, value],
};
var width = 460,
height = 300,
radius = Math.min(width, height) / 2;
var color = d3.scale.ordinal()
.range([secondColor, mainColor]);
var pie = d3.layout.pie()
.sort(null);
var arc = d3.svg.arc()
.innerRadius(radius - 100)
.outerRadius(radius - 70);
var svg = d3.select(divName).append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
//Draw the Circle
svg.append("circle")
.attr("cx", 0)
.attr("cy", 0)
.attr("r", 65)
.attr("fill", "#F6FBF3");
var path = svg.selectAll("path")
.data(pie(dataset.hddrives))
.enter().append("path")
.attr("class", "arc")
.attr("fill", function (d, i) { return color(i); })
.attr("d", arc);
svg.append("text")
.attr("dy", "0em")
.style("text-anchor", "middle")
.attr("class", "inside")
.attr("font-size", "30px")
.text(function (d) { return value; });
svg.append("text")
.attr("dy", "1.5em")
.style("text-anchor", "middle")
.attr("class", "data")
.text(function (d) { return nomeGtin; });
}
I tried messing around with the attr values and the arc value, but without success, any ideas on how to approach this? Thanks
That isn't much of a donut chart, it's now a stacked bar chart (with a single bar). The pie and arc helpers aren't much help for that, they are concerned with calculating angles and circular things; you are now dealing with rectangles. d3.stack could help, but is probably overkill. Here's a quicky where I've just done the math (ie positioning) myself:
<!DOCTYPE html>
<html>
<head>
<script data-require="d3#3.5.17" data-semver="3.5.17" src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.js"></script>
</head>
<body>
<script>
var width = 500,
height = 200,
w = 300,
h = 100;
var svg = d3.select('body')
.append('svg')
.attr('width', width)
.attr('height', height);
var total = 0,
l = 0,
// fake random data
raw = d3.range(5).map(function(d){
var v = Math.random() * 10;
total += v;
return v;
}),
// calculate percents and cumulative position
data = raw.map(function(d){
var rv = {
v: d,
l: l,
p: d/total
}
l += rv.p;
return rv;
});
// scale and color
var s = d3.scale.linear()
.range([0, w])
.domain([0, 1]),
c = d3.scale.category20();
svg.selectAll('rect')
.data(data)
.enter()
.append('rect')
.attr('x', function(d){
return s(d.l) + width/2 - w/2; // place based on cumulative
})
.attr('width', function(d){
return s(d.p); // width from scale
})
.attr('height', h)
.attr('y', height/2 - h/2)
.style('fill', function(d,i){
return c(i);
})
.style('stroke', 'white')
.style('stroke-width', '2px');
</script>
</body>
</html>

D3.js pie chart to show the percentage of sales in each quarter

Using d3.js I want to make a chart showing each item as pie chart displaying the quarterly sale of tickets for different programs
here is the sample picture of chart I want to make,each segment in the chart represents ticket for a program and its percentage of sales in each quarter. Now using this link enter link description here
I have made a graph but which is not exactly the one I needed.Is there any charts available in d3.js to show a graph as I mentioned in the picture or we need to customize it to get a graph like that.
Is there any charts available in d3.js to show a graph as I mentioned
in the picture or we need to customize it to get a graph like that?
No there isn't a ready made solution, d3 as the comment on the question notes is a collection of methods for manipulating the DOM, this allows a great deal of flexibility in creating custom visualizations (users aren't as limited as with many ready-made solutions that only allow defined modifications). Consequently, yes, you can make a chart like that in d3 taking elements and ideas from both scatter plot and pie chart implementations with d3 to make your chart.
This answer shows one approach that could be used in creating such a graph. Ideally it can provide ideas in crafting your own visualization that meets your need.
First, you need a mechanism to make variable sized pie charts and to place them - arguably this is the hardest part (after that you just have a scatter plot that's easier to manipulate). This requires some thought as to data structure, I've used a structure such as:
var data = [
{x:100,y:100,radius:20,slices:[1,5]},
{x:150,y:180,radius:10,slices:[1,2,3,4]},
You could add other properties as needed, all that this does is specify an x and y coordinate for the pie chart center, a radius for the pie chart, and the values of the wedges for each pie chart.
With that, you can append a group element (g) to your svg, one for each pie chart (or item in the data array) using a standard enter cycle in d3, positioning the groups as we go:
var pies = svg.selectAll("g")
.data(data)
.enter()
.append("g")
.property("radius",function(d) { return d.radius; })
.attr("transform",function(d) { return "translate("+d.x+","+d.y+")"; });
Because the data array used to append the wedges themselves will only include the wedge values, we can save the radius property as a property of the group and access that when appending the wedges:
pies.selectAll()
.data(function(d){ return pie(d.slices); })
.enter()
.append("path")
.attr("d",function(d) {
var radius = d3.select(this.parentNode).property("radius");
arc.outerRadius(radius);
return arc(d) })
.attr("fill",function(d,i){
return color[i];
});
A basic example might look like this:
var data = [
{x:100,y:100,radius:20,slices:[1,5]},
{x:150,y:180,radius:10,slices:[1,2,3,4]},
{x:180,y:130,radius:30,slices:[1,2,3,4,5,6,7]},
{x:50,y:50,radius:15,slices:[5,3]},
{x:50,y:180,radius:40,slices:[6,3]}
]
var width = 500;
var height = 300;
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var arc = d3.arc()
.innerRadius(0)
.outerRadius(50);
var pie = d3.pie()
.sort(null)
.value(function(d) { return d; });
var color = d3.schemeCategory10;
// Append a group for each pie chart, it will store the radius of each pie as a property
var pies = svg.selectAll("g")
.data(data)
.enter()
.append("g")
.property("radius",function(d) { return d.radius; })
.attr("transform",function(d) { return "translate("+d.x+","+d.y+")"; });
// draw each pie wedge, using the slices property of the data bound to the parent g
pies.selectAll()
.data(function(d){ return pie(d.slices); })
.enter()
.append("path")
.attr("d",function(d) {
var radius = d3.select(this.parentNode).property("radius");
arc.outerRadius(radius);
return arc(d) })
.attr("fill",function(d,i){
return color[i];
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
If you want to set each circle to have it's own color scheme, a few options might be available. If every pie has only two colors, you could assign a fill to the parent group and use the wedge increment to set transparency, creating lighter wedges such as in your image:
var data = [
{x:100,y:100,radius:20,slices:[1,5]},
{x:150,y:180,radius:10,slices:[1,2]},
{x:180,y:130,radius:30,slices:[1,7]},
{x:50,y:50,radius:15,slices:[5,3]}
]
var width = 500;
var height = 300;
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var arc = d3.arc()
.innerRadius(0)
.outerRadius(50);
var pie = d3.pie()
.sort(null)
.value(function(d) { return d; });
var color = ["steelblue","orange","pink","crimson"]
// Append a group for each pie chart, it will store the radius of each pie as a property
var pies = svg.selectAll("g")
.data(data)
.enter()
.append("g")
.property("radius",function(d) { return d.radius; })
.attr("fill",function(d,i) { return color[i] })
.attr("transform",function(d) { return "translate("+d.x+","+d.y+")"; });
// draw each pie wedge, using the slices property of the data bound to the parent g
pies.selectAll()
.data(function(d){ return pie(d.slices); })
.enter()
.append("path")
.attr("d",function(d) {
var radius = d3.select(this.parentNode).property("radius");
arc.outerRadius(radius);
return arc(d) })
.attr("opacity",function(d,i){
return 1-i*0.2;
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
Other options are available, such as storing a local variable, storing the color as a property as we did for radius, or modifying our data structure to include a color for each wedge:
var data = [
{x:100,y:100,radius:20,
slices:[{value:1,color:"steelblue"},{value:5,color:"lightblue"} ]},
{x:150,y:180,radius:10,
slices:[{value:1,color:"crimson"},{value:2,color:"pink"}]},
{x:180,y:130,radius:30,
slices:[{value:1,color:"lawngreen"},{value:7,color:"darkgreen"}]}
]
var width = 500;
var height = 300;
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var arc = d3.arc()
.innerRadius(0)
.outerRadius(50);
var pie = d3.pie()
.sort(null)
.value(function(d) { return d.value; });
// Append a group for each pie chart, it will store the radius of each pie as a property
var pies = svg.selectAll("g")
.data(data)
.enter()
.append("g")
.property("radius",function(d) { return d.radius; })
.attr("transform",function(d) { return "translate("+d.x+","+d.y+")"; });
// draw each pie wedge, using the slices property of the data bound to the parent g
pies.selectAll()
.data(function(d){ return pie(d.slices); })
.enter()
.append("path")
.attr("d",function(d) {
var radius = d3.select(this.parentNode).property("radius");
arc.outerRadius(radius);
return arc(d) })
// remember that d3.pie creates it's own data array, thus using d.data.property:
.attr("fill",function(d){ return d.data.color; })
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
Now we can adapt and implement characteristics of a scatter plot such as scales and axes. This would be the same for any other scatter plot essentially, we would scale the max and min (or a defined range) for the x and y scales, and add the axes. Altogether, that might look something like:
var data = [
{x:100,y:100,radius:10,slices:[1,5]},
{x:150,y:180,radius:10,slices:[1,2,3,4]},
{x:180,y:110,radius:30,slices:[1,2,3,4,5,6,7]},
{x:50,y:100,radius:15,slices:[5,3]},
{x:50,y:180,radius:40,slices:[6,3]}
]
var width = 500;
var height = 300;
var margin = {left:30,right:10,top:30,bottom:30}
var xScale = d3.scaleLinear()
.range([0,width-margin.left-margin.right])
.domain([0,d3.max(data,function(d) { return d.x + 20 }) ]);
var yScale = d3.scaleLinear()
.range([height-margin.top-margin.bottom,0])
.domain([0,d3.max(data,function(d) { return d.y + 20}) ]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var g = svg.append("g")
.attr("transform", "translate("+margin.left+","+margin.top+")")
var xAxis = d3.axisBottom(xScale);
g.append("g")
.attr("transform", "translate(0,"+(height-margin.bottom-margin.top)+")")
.call(xAxis);
var yAxis = d3.axisLeft(yScale);
g.append("g")
.call(yAxis);
var arc = d3.arc()
.innerRadius(0)
.outerRadius(50);
var pie = d3.pie()
.sort(null)
.value(function(d) { return d; });
var color = d3.schemeCategory10;
var pies = g.selectAll(null)
.data(data)
.enter()
.append("g")
.property("radius",function(d) { return d.radius; })
.attr("transform",function(d) { return "translate("+xScale(d.x)+","+yScale(d.y)+")"; });
pies.selectAll()
.data(function(d){ return pie(d.slices); })
.enter()
.append("path")
.attr("d",function(d) {
var radius = d3.select(this.parentNode).property("radius");
arc.outerRadius(radius);
return arc(d) })
.attr("fill",function(d,i){
return color[i];
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
Adding grid lines, legends, mouse over functionality, and other features should be relatively straightforward now - look at scatterplot examples with d3 to see how these and other features might be implemented, modifying a scatterplot of cirlces is about the same as modifying a scatterplot of pie charts.
From the sample provided by #Andrew Reid I have made it , the sample code for reference is posted here
<html>
<head>
<title>TODO supply a title</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
</head>
<body>
<script>
var data = [
{x: 170, y: 160, radius: 20, slices: [3, 4]},
{x: 180, y: 40, radius: 30, slices: [ 6, 7]},
{x: 50, y: 80, radius: 20, slices: [5, 3]},
{x: 50, y: 180, radius: 40, slices: [6, 3]}
]
var width = 500;
var height = 300;
var margin = {left: 30, right: 10, top: 30, bottom: 30}
var xScale = d3.scaleLinear()
.range([0, width - margin.left - margin.right])
.domain([0, d3.max(data, function (d) {
return d.x + 20
})]);
var yScale = d3.scaleLinear()
.range([height - margin.top - margin.bottom, 0])
.domain([0, d3.max(data, function (d) {
return d.y + 20
})]);
xMid=d3.max(xScale.domain())/2;
yMid=d3.max(yScale.domain())/2;
console.log(xMid,yMid)
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var g = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
var xAxis = d3.axisBottom(xScale);
g.append("g")
.attr("transform", "translate(0," + (height - margin.bottom - margin.top) + ")")
.call(xAxis);
var yAxis = d3.axisLeft(yScale);
g.append("g")
.call(yAxis);
var lineX= g.append("line")
.attr("x1", 0)
.attr("x2", 500)
.attr("y1", yMid+20)
.attr("y2", yMid+20)
.attr("stroke-width", 1)
.attr("stroke", "black")
.attr("stroke-dasharray", "7,7");
var liney= g.append("line")
.attr("x1", xMid+130)
.attr("x2", xMid+130)
.attr("y1", -10)
.attr("y2", 245)
.attr("stroke-width", 1)
.attr("stroke", "black")
.attr("stroke-dasharray", "7,7");
var arc = d3.arc()
.innerRadius(0)
.outerRadius(50);
var pie = d3.pie()
.sort(null)
.value(function (d) {
return d;
});
var colors = d3.schemeCategory20;
var color = ["steelblue","orange","green","red"]
var pies = g.selectAll(null)
.data(data)
.enter()
.append("g")
.property("radius", function (d) {
return d.radius;
})
.attr("transform", function (d) {
return "translate(" + xScale(d.x) + "," + yScale(d.y) + ")";
})
.attr("fill", function (d, i) {
return color[i];
});
pies.selectAll()
.data(function (d) {
return pie(d.slices);
})
.enter()
.append("path")
.attr("d", function (d) {
var radius = d3.select(this.parentNode).property("radius");
arc.outerRadius(radius);
return arc(d)
})
.attr("opacity",function(d,i){ return 1-i*0.7; });
</script>
</body>

Get bounding box of individual countries from topojson

I want to get bounding boxes for each country from a topojson, but when I add them as svg rectangles they are bundled together towards 0,0.
Ive re-read the API and played around with the order of the bound coordinates, but that didn't change anything! Also, I tried to use the SVG method getBBox() on the country paths but that produced the same result.
Any ideas?
var width = 700,
height = 400,
bboxes = [];
d3.queue()
.defer(d3.json, "data/world.topojson")
.await(ready);
//Map projection
var proj = d3.geoMercator()
.scale(100)
.center([-0.0018057527730242487, 11.258678472759552]) //projection center
.translate([width / 2, height / 2]) //translate to center the map in view
//Generate paths based on projection
var myPath = d3.geoPath().projection(proj);
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height");
//Group for the map features
var map = svg.append("g")
.attr("class", "map");
function ready(error, geodata) {
if (error) return console.log(error); //unknown error, check the console
//Create a path for each map feature in the data
map.selectAll("path")
.data(topojson.feature(geodata, geodata.objects.subunits).features) //generate features from TopoJSON
.enter()
.append("path")
.attr("class", "country")
.attr("id", function(d) {
return d.id;
})
.attr("d", myPath);
bboxes = boundingExtent(topojson.feature(geodata, geodata.objects.subunits).features);
svg.selectAll("rect")
.data(bboxes)
.enter()
.append("rect")
.attr("id", function(d){
return d.id;
})
.attr("class", "bb")
.attr("x1", function(d) {
return d.x;
})
.attr("y1", function(d) {
return d.y;
})
.attr("width", function(d) {
return d.width;
})
.attr("height", function(d) {
return d.height;
})
}
function boundingExtent(features) {
var bounds= [];
for (var x in features) {
var boundObj = {};
thisBounds = myPath.bounds(features[x]);
boundObj.id = features[x].id;
boundObj.x = thisBounds[0][0];
boundObj.y = thisBounds[0][1];
boundObj.width = thisBounds[1][0] - thisBounds[0][0];
boundObj.height = thisBounds[1][1] - thisBounds[0][1];
boundObj.path = thisBounds;
bounds.push(boundObj)
}
return bounds;
}
function boundExtentBySvg(){
var countries = svg.selectAll(".country")
countries.each(function(d){
var box = d3.select(this).node().getBBox();
bboxes.push({id: d.id, x: box.x, y : box.y, width: box.width, height : box.height})
})
}
In these lines:
.attr("x1", function(d) {
return d.x;
})
.attr("y1", function(d) {
return d.y;
})
rect does not have an attribute of x1 or y1, I think you meant just x and y.
Here's your code running (note, I switched out the topojson file which caused slight code changes):
<!DOCTYPE html>
<html>
<head>
<script data-require="d3#4.0.0" data-semver="4.0.0" src="https://d3js.org/d3.v4.min.js"></script>
<script data-require="topojson.min.js#3.0.0" data-semver="3.0.0" src="https://unpkg.com/topojson#3.0.0"></script>
</head>
<body>
<svg width="700" height="400"></svg>
<script>
var width = 700,
height = 400,
bboxes = [];
d3.queue()
.defer(d3.json, "https://unpkg.com/world-atlas#1/world/110m.json")
.await(ready);
//Map projection
var proj = d3.geoMercator()
.scale(100)
.center([-0.0018057527730242487, 11.258678472759552]) //projection center
.translate([width / 2, height / 2]) //translate to center the map in view
//Generate paths based on projection
var myPath = d3.geoPath().projection(proj);
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height");
//Group for the map features
var map = svg.append("g")
.attr("class", "map");
function ready(error, geodata) {
if (error) return console.log(error); //unknown error, check the console
//Create a path for each map feature in the data
map.selectAll("path")
.data(topojson.feature(geodata, geodata.objects.countries).features) //generate features from TopoJSON
.enter()
.append("path")
.attr("class", "country")
.attr("id", function(d) {
return d.id;
})
.attr("d", myPath);
bboxes = boundingExtent(topojson.feature(geodata, geodata.objects.countries).features);
svg.selectAll("rect")
.data(bboxes)
.enter()
.append("rect")
.attr("id", function(d) {
return d.id;
})
.attr("class", "bb")
.attr("x", function(d) {
return d.x;
})
.attr("y", function(d) {
return d.y;
})
.attr("width", function(d) {
return d.width;
})
.attr("height", function(d) {
return d.height;
})
.style("fill", "none")
.style("stroke", "steelblue");
}
function boundingExtent(features) {
var bounds = [];
for (var x in features) {
var boundObj = {};
thisBounds = myPath.bounds(features[x]);
boundObj.id = features[x].id;
boundObj.x = thisBounds[0][0];
boundObj.y = thisBounds[0][1];
boundObj.width = thisBounds[1][0] - thisBounds[0][0];
boundObj.height = thisBounds[1][1] - thisBounds[0][1];
boundObj.path = thisBounds;
bounds.push(boundObj)
}
console.log(bounds)
return bounds;
}
function boundExtentBySvg() {
var countries = svg.selectAll(".country")
countries.each(function(d) {
var box = d3.select(this).node().getBBox();
bboxes.push({
id: d.id,
x: box.x,
y: box.y,
width: box.width,
height: box.height
})
})
}
</script>
</body>
</html>

D3JS makes date duplicates

I have this d3js code:
var tooltip = tooltipd3();
var svg = d3.select("svg#svg-day"),
margin = {
top: 20,
right: 30,
bottom: 30,
left: 25,
padding: 15
},
width = 700 - margin.left - margin.right,
height = 300 - margin.top - margin.bottom;
// parse the periodo / time
var parseTime = d3.timeParse("%Y-%m-%d");
// set the ranges
var x = d3.scaleTime().range([0, width - margin.padding]);
var y = d3.scaleLinear().range([height, 0]);
// define the area
var area = d3.area()
.x(function(d) {
return x(d.periodo) + (margin.left + margin.padding);
})
.y0(height)
.y1(function(d) {
return y(d.guadagno);
});
// define the line
var valueline = d3.line()
.x(function(d) {
return x(d.periodo) + (margin.left + margin.padding);
})
.y(function(d) {
return y(d.guadagno);
});
var div = d3.select("svg#svg-day")
.append("div") // declare the tooltip div
.attr("class", "tooltip") // apply the 'tooltip' class
.style("opacity", 0);
// get the data
d3.csv(base_url() + 'graph/getStatementsDaily/', function(error, data) {
if (error) throw error;
$('.graph-loading').hide();
// format the data
data.forEach(function(d) {
d.periodo = parseTime(d.periodo)
d.guadagno = +d.guadagno;
});
// scale the range of the data
x.domain(d3.extent(data, function(d) {
return d.periodo;
}));
y.domain([0, d3.max(data, function(d) {
return d.guadagno + ((d.guadagno / 100) * 10); // 10% in più sulla scala numerica
})]);
// add the area
svg.append("path")
.data([data])
.attr("class", "area")
.attr("d", area);
// add the valueline path.
svg.append("path")
.data([data])
.attr("class", "line")
.attr("d", valueline);
// Add the scatterplot
svg.selectAll("dot")
.data(data)
.enter().append("circle")
.attr("class", "dot")
.attr("r", 3)
.attr("cx", function(d) {
return x(d.periodo) + (margin.left + margin.padding);
})
.attr("cy", function(d) {
return y(d.guadagno);
})
.on('mouseover', function(d) {
var html = '<h5>' + d.guadagno + ' €</h5>';
tooltip.mouseover(html); // pass html content
})
.on('mousemove', tooltip.mousemove)
.on('mouseout', tooltip.mouseout);
// add the X Axis
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(" + (margin.left + margin.padding) + "," + (height) + ")")
//HERE IS THE DATES CODE
.call(d3.axisBottom(x).tickFormat(d3.timeFormat("%d/%m")))
// add the Y Axis
svg.append("g")
.attr("class", "y axis")
.attr("transform", "translate (" + (margin.left + margin.padding) + " 0)")
.call(d3.axisLeft(y));
});
The dates care coming from a CSV file that has this format:
periodo,guadagno
2017-05-08,0.0
2017-05-09,0.5385
2017-05-10,0.0
2017-05-11,0.0
2017-05-12,0.0
2017-05-13,0.5680
2017-05-14,0.0
2017-05-15,0.0
The result is fine with lots of dates, but with 7 dates I get duplicates as you can see here:
Why is this?? And how do I fix it?
This is something that bothers a lot of people new to D3: the ticks in the axis, specially when using a time scale, are automatically generated. In your case, given the date interval in your domain, it coincidentally ended up creating two ticks for each day. But pay attention to this: those ticks represent different times (hours) in the same day (you can see that if you remove the tickFormat in the axis generator).
Let's see your code generating the x axis:
var svg = d3.select("svg");
var data = d3.csvParse(d3.select("#csv").text());
var parseTime = d3.timeParse("%Y-%m-%d");
data.forEach(function(d) {
d.periodo = parseTime(d.periodo)
});
var x = d3.scaleTime()
.range([20, 480])
.domain(d3.extent(data, function(d) {
return d.periodo;
}));
var axis = d3.axisBottom(x).tickFormat(d3.timeFormat("%d/%m"))(svg.append("g").attr("transform", "translate(0,50)"));
pre {
display: none;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="500"></svg>
<pre id="csv">periodo,guadagno
2017-05-08,0.0
2017-05-09,0.5385
2017-05-10,0.0
2017-05-11,0.0
2017-05-12,0.0
2017-05-13,0.5680
2017-05-14,0.0
2017-05-15,0.0</pre>
As you can see, there are two ticks for each day (remember, for different hours).
Let's show that this is a coincidence: This is the same code, but changing the last date for 2017-05-20:
var svg = d3.select("svg");
var data = d3.csvParse(d3.select("#csv").text());
var parseTime = d3.timeParse("%Y-%m-%d");
data.forEach(function(d) {
d.periodo = parseTime(d.periodo)
});
var x = d3.scaleTime()
.range([20, 480])
.domain(d3.extent(data, function(d) {
return d.periodo;
}));
var axis = d3.axisBottom(x).tickFormat(d3.timeFormat("%d/%m"))(svg.append("g").attr("transform", "translate(0,50)"));
pre {
display: none;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="500"></svg>
<pre id="csv">periodo,guadagno
2017-05-08,0.0
2017-05-09,0.5385
2017-05-10,0.0
2017-05-11,0.0
2017-05-12,0.0
2017-05-13,0.5680
2017-05-14,0.0
2017-05-20,0.0</pre>
Back to your code.
The solution is quite simple: using intervals. Let's set the interval for each tick:
d3.axisBottom(x).ticks(d3.timeDay)
Here is the same code with that change only:
var svg = d3.select("svg");
var data = d3.csvParse(d3.select("#csv").text());
var parseTime = d3.timeParse("%Y-%m-%d");
data.forEach(function(d) {
d.periodo = parseTime(d.periodo)
});
var x = d3.scaleTime()
.range([20, 480])
.domain(d3.extent(data, function(d) {
return d.periodo;
}));
var axis = d3.axisBottom(x).tickFormat(d3.timeFormat("%d/%m")).ticks(d3.timeDay)(svg.append("g").attr("transform", "translate(0,50)"));
pre {
display: none;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="500"></svg>
<pre id="csv">periodo,guadagno
2017-05-08,0.0
2017-05-09,0.5385
2017-05-10,0.0
2017-05-11,0.0
2017-05-12,0.0
2017-05-13,0.5680
2017-05-14,0.0
2017-05-15,0.0</pre>

Resources