d3.js - rotate rectangle around point - d3.js

Below example will draw a rectangle, I would like to rotate the rectangle around top left corner (0,500) with anti-clockwise 15 degree and keep to coordinate system not rotate.
test()
function test() {
var margin = {left:10,top:10,right:10,bottom:10}
var width = 300
var height = 300
var svg = d3.select('body').append('svg')
svg.attr('width',width + margin.left + margin.right)
.attr('height',height + margin.top + margin.bottom)
.style('border','2px solid red')
var g = svg.append('g')
.attr('transform',`translate(${margin.left},${margin.top})`)
var xmin = 0
var xmax = 800
var xScale = d3.scaleLinear().range([0, width]);
xScale.domain([xmin,xmax]);
var ymin = 0
var ymax = 800
var yScale = d3.scaleLinear().range([height, 0]);
yScale.domain([ymin,ymax])
var fw = 200
var fh = 500
var data = [[0,0],[0,fh],[fw,fh],[fw,0]]
draw_box(g,'rect',data,xScale,yScale)
function draw_box(g,clzname,arr,xScale,yScale,col='#ffffff') {
var drawrect = function(d) {
var arr = []
arr.push('M')
for (var i=0;i<d.length;i++) {
e = d[i]
arr.push(xScale(e[0]))
arr.push(yScale(e[1]))
if (i == 0) {
arr.push('L')
}
}
arr.push('z')
var path = arr.join(' ')
return path
}
g.selectAll(clzname)
.data([arr])
.join('path')
.attr('class',clzname)
.attr('d',drawrect)
.attr('stroke','black')
.attr('fill',col)
}
}
<script src="https://d3js.org/d3.v6.min.js"></script>

Related

d3.js - join data update behavior wired

This demo try to update data when click the graph. it use the join method but looks like data and axis have been overlapped after click. the previous drawing not been cleared!
console.clear()
click_to_update()
function click_to_update() {
var svg = d3.select('body').append('svg')
.attr('width',600)
.attr('height',400)
.style('border','5px solid red')
var frame = svg.append('g').attr('class','frame')
frame.append('g').attr('class','xaxis')
frame.append('g').attr('class','yaxis')
d3.select('svg').on('click', function(){
var data = fetch_data()
refresh_graph(data)
})
var data = fetch_data()
refresh_graph(data)
function refresh_graph(data){
var svg = d3.select('svg')
var colors = d3.scaleOrdinal(d3.schemeSet3)
var margin = {left: 40,top: 10, right:10,bottom: 60},
width = +svg.attr('width') - margin.left - margin.right,
height = +svg.attr('height') - margin.top - margin.bottom;
var g = d3.select('.frame')
.attr('transform',`translate(${margin.left},${margin.top})`)
var xrange = data.map(function(d,i) { return i; })
var x = d3.scalePoint()
.domain(xrange)
.range([0, width]);
var ymax = d3.max(data,d => d.value)
var y = d3.scaleLinear()
.domain([0,ymax])
.range([height, 0]);
var drawpath = function(d,i) {
var ax = x(i)
var ay = y(d.value)
var bx = x(i)
var by = y(0)
var path = ['M',ax,ay,'L',bx,by]
return path.join(' ')
}
var g1 = g.selectAll('path')
.data(data)
.join('path')
.attr('stroke','gray')
.attr('stroke-width',1)
.style('fill', (d, i) => colors(i))
.transition()
.attr('d', drawpath)
g.selectAll('circle')
.data(data)
.join('circle')
.attr('fill','red')
.attr('cx',(d,i) => x(i))
.attr('cy',(d,i) => y(d.value))
.attr('r',5)
var xaxis = d3.select('.xaxis')
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
var yaxis = d3.select('.yaxis')
.attr('transform','translate(-5,0)')
.call(d3.axisLeft(y));
}
function fetch_data(){
var num = parseInt(Math.random()*20) + 5
var data = d3.range(num).map(d => {
return {value:Math.random()}
})
return data
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.0.0/d3.min.js"></script>

How to drag a path in d3.js

I cannot work out how to drag a path around the svg object using d3.js
Specifically, I have a normal distribution shape rendered as a path to the svg and I want to be able to click on it and drag it around the svg space (but there is nothing unique about this particular shape etc) .
I have seen examples for points, straight lines and shapes but not a path.
My simplified code is below. Unless I am way off the mark, I suspect the error is with the dragged function right at the bottom.
Javascript:
// width and height for svg object
var w = 500;
var h = 500;
/// setting up svg object
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h)
// Values for calculating pdf of normal distribution
var sigma = 4;
var mu = 0;
var N = 10;
var step = 0.1;
var dataset = [];
var x;
// creating the pdf of the normal distribution and plotting it for -N to N
var C = 1/(sigma*Math.sqrt(2*Math.PI));
for (x=-N; x < N; x += step) {
var E = (x-mu)/sigma;
E = -(E*E)/2;
var d = C*Math.exp(E);
dataset.push(d);
}
// Scales slightly over fancy, required for features stripped out
var overlap = w*0.1;
var xscale1 = d3.scale.linear().range([0, w/2+overlap]).domain([0, dataset.length-1]).clamp(true);
var xscale2 = d3.scale.linear().range([w/2-overlap, w]).domain([0, dataset.length-1]).clamp(true);
// So specifies the height as max in dataset and it takes up 1/2 the svg
var yscale = d3.scale.linear().domain([0, d3.max(dataset)]).range([h,h/2]);
var area1 = d3.svg.area()
.x(function(d,i) { return xscale1(i); })
.y0(h)
.y1(function(d,i) { return yscale(d); });
// plots filled normal distribution to svg
g1 = svg.append("path")
.datum(dataset)
.attr("class", "area1")
.attr("d", area1)
.attr("opacity",0.75);
// Problem is probably with the below line and related function dragged
d3.select("path.area1").on("drag", dragged);
function dragged() {
var dx = d3.event.dx,
dy = d3.event.dy;
d3.select(this)
.attr("transform", path => "translate(" + dx + "," + dy + ")");
}
Here is a version of your code which implements the drag:
var w = 500;
var h = 250;
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
// Values for calculating pdf of normal distribution
var sigma = 4;
var mu = 0;
var N = 10;
var step = 0.1;
var dataset = [];
var x;
// creating the pdf of the normal distribution and plotting it for -N to N
var C = 1/(sigma*Math.sqrt(2*Math.PI));
for (x=-N; x < N; x += step) {
var E = (x-mu)/sigma;
E = -(E*E)/2;
var d = C*Math.exp(E);
dataset.push(d);
}
// Scales slightly over fancy, required for features stripped out
var overlap = w*0.1;
var xscale1 = d3.scale.linear().range([0, w/2+overlap]).domain([0, dataset.length-1]).clamp(true);
var xscale2 = d3.scale.linear().range([w/2-overlap, w]).domain([0, dataset.length-1]).clamp(true);
// So specifies the height as max in dataset and it takes up 1/2 the svg
var yscale = d3.scale.linear().domain([0, d3.max(dataset)]).range([h,h/2]);
var area1 = d3.svg.area()
.x(function(d,i) { return xscale1(i); })
.y0(h)
.y1(function(d,i) { return yscale(d); });
svg.append("path")
.datum(dataset)
.attr("class", "area1")
.attr("d", area1)
.attr("opacity",0.75)
.call(d3.behavior.drag().on("drag", dragged));
function dragged(d) {
// Current position:
this.x = this.x || 0;
this.y = this.y || 0;
// Update thee position with the delta x and y applied by the drag:
this.x += d3.event.dx;
this.y += d3.event.dy;
// Apply the translation to the shape:
d3.select(this)
.attr("transform", "translate(" + this.x + "," + this.y + ")");
}
<body></body>
<script src="https://d3js.org/d3.v3.min.js"></script>
It's actually the exact same way of doing as any other drags on other types of shapes. You just apply the drag behavior on the selected node.
Here is the part in charge of the drag implementation:
svg.append("path")
.datum(dataset)
.attr("d", area1)
...
.call(d3.behavior.drag().on("drag", dragged));
function dragged(d) {
// Current position:
this.x = this.x || 0;
this.y = this.y || 0;
// Update thee position with the delta x and y applied by the drag:
this.x += d3.event.dx;
this.y += d3.event.dy;
// Apply the translation to the shape:
d3.select(this)
.attr("transform", "translate(" + this.x + "," + this.y + ")");
}
The main thing you missed is the fact that the dx and dy you receive from the event are the movements of the mouse (the "delta" of movement). These movements can't become the new position of the shape. They have to be added to the existing x and y current position of the shape.
And here is the same code but for the version 4 of d3:
var w = 500;
var h = 250;
var svg = d3.select("body").append("svg").attr("width", w).attr("height", h)
// Values for calculating pdf of normal distribution
var sigma = 4;
var mu = 0;
var N = 10;
var step = 0.1;
var dataset = [];
var x;
// creating the pdf of the normal distribution and plotting it for -N to N
var C = 1/(sigma*Math.sqrt(2*Math.PI));
for (x=-N; x < N; x += step) {
var E = (x-mu)/sigma;
E = -(E*E)/2;
var d = C*Math.exp(E);
dataset.push(d);
}
// Scales slightly over fancy, required for features stripped out
var overlap = w*0.1;
var xscale1 = d3.scaleLinear().range([0, w/2+overlap]).domain([0, dataset.length-1]).clamp(true);
var xscale2 = d3.scaleLinear().range([w/2-overlap, w]).domain([0, dataset.length-1]).clamp(true);
// So specifies the height as max in dataset and it takes up 1/2 the svg
var yscale = d3.scaleLinear().domain([0, d3.max(dataset)]).range([h,h/2]);
var area1 = d3.area()
.x(function(d,i) { return xscale1(i); })
.y0(h)
.y1(function(d,i) { return yscale(d); });
// plots filled normal distribution to svg
g1 = svg.append("path")
.datum(dataset)
.attr("class", "area1")
.attr("d", area1)
.attr("opacity",0.75)
.call(d3.drag().on("drag", dragged));
function dragged(d) {
// Current position:
this.x = this.x || 0;
this.y = this.y || 0;
// Update thee position with the delta x and y applied by the drag:
this.x += d3.event.dx;
this.y += d3.event.dy;
// Apply the translation to the shape:
d3.select(this)
.attr("transform", "translate(" + this.x + "," + this.y + ")");
}
<body></body>
<script src="https://d3js.org/d3.v4.min.js"></script>

angular2 d3: update d3 svg mouse pos to component property

D3 is used to generate a svg in an angular2 component. How to update properties x and y in component from svg event mousemove?
export class AxisComponent implements OnInit {
x:number;
y:number;
ngOnInit() {
var svgWidth=400;
var svgHeight=400;
var margin = {top:25, right:25, bottom:50, left:50};
var width = svgWidth - margin.left - margin.right;
var height = svgHeight - margin.top - margin.bottom;
var svg = d3.select('#container').append('svg')
.attr('width', svgWidth)
.attr('height',svgHeight)
.style('border', '2px solid');
svg.on("mousemove", function(){
var xy = d3.mouse(this);
this.x = xy[0];
this.y = xy[0];
});
}
Error when accessing from mousemove event:
I suspect it should be:
svg.on("mousemove", () => {
var xy = d3.mouse(svg); // or d3.mouse(d3.event.currentTarget);
this.x = xy[0];
this.y = xy[0];
Or this way:
let self = this;
svg.on("mousemove", function(){
var xy = d3.mouse(this);
self.x = xy[0];
self.y = xy[0];
});

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.

Determining coordinates in lat/lng of a point in pixels on a map after zoom has been applied

I'm trying to determine coordinates of point in pixels on a map using d3. The ultimate goal is determine which countries are currently visible on the map.
Here's the code I'm using:
method: function() {
var e = $("#" + this.getView().getId());
if (!e || e.length === 0) {
return;
}
this.width = e.width();
this.height = e.height();
this.topojson = window.topojson;
var width = this.width;
var height = this.height;
var centered;
var d3v3 = window.d3v3;
var projection = d3v3.geo.mercator()
.translate([width / 2, height / 2]);
var path = d3v3.geo.path()
.projection(projection);
var svg = d3v3.select("#" + this.byId("map").getId());
svg.selectAll("g").remove();
svg
.attr("width", width)
.attr("height", height);
svg.append("rect")
.attr("class", "background")
.attr("width", width)
.attr("height", height)
.on("click", clicked);
var g = svg.append("g");
d3v3.json(getPath("/110m_admin_0.json"), function(us) {
g.append("g")
.attr("id", "states")
.attr("class", "counties")
.selectAll("path")
.data(topojson.feature(us, us.objects["110m_admin_0_"]).features)
.enter().append("path")
.attr("d", path)
.attr("class", function(d) { return "q"+(Math.floor((Math.random() * 9) + 0)) + "-9"; });
});
var zoom = d3v3.behavior.zoom()
.on("zoom",$.proxy(function() {
var width1 = width/2;
var height1 = height/2;
var xt1 = d3v3.event.translate[0];
var yt1 = d3v3.event.translate[1];
var x = xt1;
var y = yt1;
x+=width1;
y+=height1;
var proj = projection.invert([x,y]);
proj[0]*=-1;
proj[1]*=-1;
var closestCountry = this.closestCountry(proj);
console.log(d3v3.event.scale + " | " + [x,y] + " | " + proj + " | " + closestCountry );
g.attr("transform","translate("+
d3v3.event.translate.join(",")+")scale("+d3v3.event.scale+")");
g.selectAll("path")
.attr("d", path.projection(projection));
},this));
svg.call(zoom);
}
This code works when the zoom level is 1, but fails as soon as the zoom level changes.
Here's a few variations I've tried.
1)
var x = d3v3.event.translate[0];
var y = d3v3.event.translate[1];
x+=width/2;
y+=height/2;
var proj = projection.invert([x,y]);
2)
var x = d3v3.event.translate[0]/d3v3.event.scale;
var y = d3v3.event.translate[1]/d3v3.event.scale;
x+=width/2;
y+=height/2;
var proj = projection.invert([x,y]);
3)
var x = d3v3.event.translate[0];
var y = d3v3.event.translate[1];
x+=width/2*d3v3.event.scale;
y+=height/2*d3v3.event.scale;
var proj = projection.invert([x,y]);
4)
var x = d3v3.event.translate[0]/d3v3.event.scale;
var y = d3v3.event.translate[1]/d3v3.event.scale;
x+=width/2*d3v3.event.scale;
y+=height/2*d3v3.event.scale;
var proj = projection.invert([x,y]);
One thing I have noticed is that whatever the zoom level, projection.invert() for the same [x,y] point (e.g. [1200,600])
always return the same lng/lat. Furthermore, none of the attempts I've made manage to keep [x,y] constant at varying zoom level so there is something else at play here outside of the translation and scale. I suspect it might have something to do with the projection, but I've still not figured out what.
After zoom has been applied, I haven't found a way to go from a point in pixel to a coordinate in lng/lat or from a point in pixel to a country.
That being said, I found an alternate method that works well too. I have a list of all the countries and their centroids in lng/lat. From that list, I resolve their position in pixel and adjust them based on scale and translation. Finally, I find which one is the closest to the center. Here's the code:
closest: function() {
var p2 = [0,0];
var d = [width/2,height/2];
var scale = d3v3.event.scale;
var translate = d3v3.event.translate;
var min = Number.MAX_VALUE;
var minCountry = null;
for ( var key in this.centroids) {
var p1 = projection(this.centroids[key]);
p1[0]*=scale;
p1[1]*=scale;
p1[0]+=translate[0]-d[0];
p1[1]+=translate[1]-d[1];
var distance = Math.sqrt(Math.pow(p2[0] - p1[0],2) + Math.pow(p2[1] - p1[1],2));
if (distance < min) {
min = distance;
minCountry = key;
}
}
return [ minCountry, min ];
}

Resources