How to rotate SVG using d3.drag()? - d3.js

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>

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>

Rotating an SVG image using D3.JS not working

I've been trying to rotate some gears in an svg image clockwise repeatedly using D3.js but I can't seem to understand what's wrong with the code or the image. I was able to translate and rotate each gear using this code below...
d3.selectAll(".st6")
.transition()
.delay(function(d, i, n) { return i * 50; })
.on("start", function repeat() {
d3.active(this)
.transition()
.duration(2500)
.attr('transform', 'rotate(0)')
.transition() //And rotate back again
.duration(2500)
.attr('transform' , 'rotate(90) ')
.on("start", repeat); //at end, call it again to create infinite loop
});
But when I tried using the same code that did rotate a text repeatedly for me, the image became static and not moving again...
here is the code that rotate a text repeatedly for me...
var width = 600;
var height = 300;
var holder = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
//draw the text
holder.append("text")
.style("fill", "black")
.style("font-size", "56px")
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.attr("transform", "translate(300,150) rotate(0)")
.text("Hi, how r u doing");
var i = 0;
var timeInterval = 10;
setInterval(function(){
i += 1;
update(i % 360)
}, timeInterval);
var n;
// update the element
function update(n) {
// rotate the text
holder.select("text")
.attr("transform", "translate(300,150) rotate("+n+")");
}
Here is what I tried replicating the above method but to no avail...
var width = 600;
var height = 300;
var holder = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
holder.append("svg:image")
.attr("image-anchor", "middle")
.attr("xlink:href", "produc.svg")
.attr("transform", "translate(300,150) rotate(0)")
// Initial starting angle of the text
var i = 0;
var timeInterval = 10;
setInterval(function(){
i += 1;
update(i % 360)
},timeInterval);
var n;
// update the element
function update(n) {
// rotate the text
holder.select("text")
.attr("transform", "translate(300,150) rotate("+n+")");
}
Here is a working example on CodePenclick here
As for the problem with your code (without considering if it is "a sledgehammer to crack a nut"):
You're taking working code that rotates text and not fully updating it to reflect that you are now working with an image. You have updated the append statement to append an image, but you haven't updated the update function to select that image - it's still looking for a text element:
function update(n) {
// rotate the text
holder.select("text")
.attr("transform", "translate(300,150) rotate("+n+")");
}
Since there is no longer a text element, this function doesn't select anything and consequently, doesn't set any element's transform. AS noted in the comment you need to select the image:
function update(n) {
// rotate the text
holder.select("image")
.attr("transform", "translate(300,150) rotate("+n+")");
}
As seen below:
var width = 600;
var height = 300;
var holder = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
holder.append("svg:image")
.attr("image-anchor", "middle")
.attr("xlink:href", "https://dev.w3.org/SVG/tools/svgweb/samples/svg-files/compass.svg")
.attr("transform", "translate(200,20) rotate(0)")
.attr("width", 100)
// Initial starting angle of the text
var i = 0;
var timeInterval = 10;
setInterval(function(){
i += 1;
update(i % 360)
},timeInterval);
// update the element
function update(n) {
// rotate the text
holder.select("image")
.attr("transform", "translate(200,20) rotate("+n+",50,50)");
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

Live Horizontal Bar Chart keeps adding nodes

I am trying to make a horizontal bar chart for test purposes which changes data in real time. I notice that nodes keep adding.
var dataset = [ 5, 10, 15, 20, 25 ]
var w = 1200;
var h = 500;
var barPadding = 1;
var container = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h)
.append("g");
var rects = container.selectAll("rect")
var yScale = d3.scaleLinear()
.range([h, 0])
function draw(dataset, translate){
yScale.domain([0, d3.max(dataset)])
rects.data(dataset)
.enter()
.append("rect")
.attr("x", function(d, i){
return i * 12 + translate
})
.attr("y", function(d){
return yScale(d)
})
.attr("width", 11)
.attr("height", function(d) { return (h - yScale(d)) })
rects.exit().remove()
}
var translate = 0
setInterval(function(){
container.attr("transform", "translate("+-translate+",0)")
dataset.push(Math.floor(Math.random() * 30))
draw(dataset, translate)
translate = translate + 12
dataset.shift()
}, 1000)
rects.exit.remove() doesn't seem to work, how can I fix this? I could not find any examples of live horizontal bar charts on d3 v5 which is what I am using here
Right now you don't have a proper update selection, which is:
var rects = container.selectAll("rect")
.data(dataset);
Because of that, all rectangles belong to the enter selection.
Here is the updated code, with the size of the update selection in the console:
var dataset = [5, 10, 15, 20, 25]
var w = 500;
var h = 300;
var barPadding = 1;
var container = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h)
.append("g");
var yScale = d3.scaleLinear()
.range([h, 0]);
var translate = 0
draw(dataset, translate)
function draw(dataset, translate) {
yScale.domain([0, d3.max(dataset)])
var rects = container.selectAll("rect")
.data(dataset);
rects.enter()
.append("rect")
.merge(rects)
.attr("x", function(d, i) {
return i * 12 + translate
})
.attr("y", function(d) {
return yScale(d)
})
.attr("width", 11)
.attr("height", function(d) {
return (h - yScale(d))
})
rects.exit().remove();
console.log("the update size is: " + rects.size())
}
setInterval(function() {
container.attr("transform", "translate(" + -translate + ",0)")
dataset.push(Math.floor(Math.random() * 30))
draw(dataset, translate)
translate = translate + 12
dataset.shift()
}, 1000)
<script src="https://d3js.org/d3.v5.min.js"></script>

Expanding D3 Pie Chart to fill SVG

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;
}

d3js how to get rotated rect's corner coordinates?

I'm pretty new to d3js and feeling a little overwhelmed here. I'm trying to figure out how to query a rotated rectangle's corner coordinates so i can place a circle on that location (eventually I'm going to use that as a starting coordinate for a line to link to other nodes).
Here is an image showing what I'm trying to do:
Currently I'm getting the circle on the left of the svg boundary below, I'm trying to place it roughly where the x is below.
Here is my code for the circle:
let rx = node.attr("x");
let ry = node.attr("y");
g.append("circle")
.attr("cx",rx)
.attr("cy",ry)
.attr("r",5);
Here is my jsFiddle: jsFiddle and a Stack Overflow snippet
let d3Root = 'd3-cpm';
let w = document.documentElement.clientWidth;
let h = document.documentElement.clientHeight;
//TODO put type any
let eData = {
width: 180,
height: 180,
padding: 80,
fill: '#E0E0E0',
stroke: '#c3c5c5',
strokeWidth: 3,
hoverFill: '#1958b5',
hoverStroke: '#0046ad',
hoverTextColor: '#fff',
rx: 18,
ry: 18,
rotate: 45,
label: 'Decision Node',
textFill: 'black',
textHoverFill: 'white'
};
let cWidth;
let cHeight = h;
d3.select(d3Root)
.append("div")
.attr("id", "d3-root")
.html(function () {
let _txt = "Hello From D3! <br/>Frame Width: ";
let _div = d3.select(this);
let _w = _div.style("width");
cWidth = parseInt(_div.style("width"));
_txt += cWidth + "<br/> ViewPort Width: " + w;
return _txt;
});
let svg = d3.select(d3Root)
.append("svg")
.attr("width", cWidth)
.attr("height", cHeight)
.call(d3.zoom()
//.scaleExtent([1 / 2, 4])
.on("zoom", zoomed));
;
let g = svg.append("g")
.on("mouseover", function (d) {
d3.select(this)
.style("cursor", "pointer");
d3.select(this).select("rect")
.style("fill", eData.hoverFill)
.style("stroke", eData.hoverStroke);
d3.select(this).select("text")
.style("fill", eData.textHoverFill);
})
.on("mouseout", function (d) {
d3.select(this)
.style("cursor", "default");
d3.select(this).select("rect")
.style("fill", eData.fill)
.style("stroke", eData.stroke);
d3.select(this).select("text")
.style("fill", eData.textFill);
});
let node = g.append("rect")
.attr("width", eData.width)
.attr("height", eData.height)
.attr("fill", eData.fill)
.attr("stroke", eData.stroke)
.attr("stroke-width", eData.strokeWidth)
.attr("rx", eData.rx)
.attr("ry", eData.ry)
.attr("y", eData.padding)
.attr('transform', function () {
let _x = calcXLoc();
console.log(_x);
return "translate(" + _x + "," + "0) rotate(45)";
})
.on("click", ()=> {
console.log("rect clicked");
d3.event.stopPropagation();
//this.nodeClicked();
});
let nText = g.append('text')
.text(eData.label)
.style('fill', eData.textFill)
.attr('x', calcXLoc() - 50)
.attr('y', eData.width + 10)
.attr("text-anchor", "middle")
.on("click", ()=> {
console.log("text clicked");
d3.event.stopPropagation();
//this.nodeClicked();
});
let rx = node.attr("x");
let ry = node.attr("y");
g.append("circle")
.attr("cx",rx)
.attr("cy",ry)
.attr("r",5);
function calcXLoc() {
return (cWidth / 2 - eData.width / 2) + eData.width;
}
function zoomed() {
g.attr("transform", d3.event.transform);
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<d3-cpm></d3-cpm>
You're applying a transform to your rect to position and rotate it. It has no x attribute, so that comes back as undefined. This gets you slightly closer:
let rx = parseInt(node.attr("x"), 10) | 0;
let ry = parseInt(node.attr("y"), 10) | 0;
let height = parseInt(node.attr("height"), 10) | 0;
let transform = node.attr("transform");
g.append("circle")
.attr("cx",rx + height)
.attr("cy",ry + height)
.attr("transform", transform)
.attr("r",5);
But note that this is going to get kind of clunky and difficult to deal with - it'd be better if your data was modeled in such a way that the circular points were handled in there as well and could be somehow derived/transformed consistently....
Updated fiddle: https://jsfiddle.net/dcw48tk6/7/
Image:

Resources