I am working on a scatterplot in d3 where I need to be able to update the yAxis domain when I click on a button.
This is what I have right now. This is the lowest and highest value.
yAxis = d3.scaleLinear().rangeRound([height, 0]);
yAxis.domain([23500, 29600]);
How can I change the Domain to like domain([26500, 33600]) when I click on a button?
Do I need to add it to the buttons with a click function?
d3.select('#data2010').on('click', function () {
Or is there a way to automatically look for the lowest and highest value and update it?
This is a simple example on how to update a domain and rescale an axis.
You need to get something from the user to be able to know when to rescale the axis. So that click event has to come from some mode of input like a button, radio button, checkbox, etc.
First off, you need to update the domain for the scale that you're using for your y-axis.
Call .transition() with a .duration() with a set timeframe for the axis to transition when the user wants it to rescale.
You can accomplish the above with something like the following (You call the function when your button is clicked):
function rescale() {
y.domain([26500, 33600])
svg.selectAll("g.yaxis")
.transition().duration(1000)
.call(d3.axisLeft(y));
}
Check working snippet below:
var margin = {
top: 20,
right: 80,
bottom: 30,
left: 50
},
width = 500 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var y = d3.scaleLinear().range([height, 0]);
y.domain([23500, 29600]);
svg.append("g")
.attr("class", "yaxis")
.call(d3.axisLeft(y));
function rescale() {
y.domain([26500, 33600])
svg.selectAll("g.yaxis")
.transition().duration(1000)
.call(d3.axisLeft(y));
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.8.0/d3.min.js"></script>
<button onclick="rescale();">Rescale</button>
Related
I am creating a US Map and I have a series of ACTUAL coordinates of some places in US. I would like to put a point or bubble on the right spot in the map. How do I scale/translate these?
This is what I get:
With what I have tried:
function USAPlot(divid, data) {
var margin = { top: 20, right: 20, bottom: 30, left: 50 },
width = 1040 - margin.left - margin.right,
height = 700 - margin.top - margin.bottom;
// formatting the data
data.forEach(function (d) {
d.loc = d.location;
d.count = d.count;
d.lat = d.latitude;
d.lon = d.longitude;
});
var svg = d3.select(divid)
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
;
var path = d3.geoPath();
var projection = d3.geoMercator()
.scale(200)
.translate([margin.left + width / 2, margin.top + height / 2])
d3.json("https://d3js.org/us-10m.v1.json", function (error, us) {
if (error) throw error;
svg.append("g")
.attr("class", "states")
.attr("fill-opacity", 0.4)
.selectAll("path")
.data(topojson.feature(us, us.objects.states).features)
.enter().append("path")
.attr("d", path);
svg.append("path")
.attr("class", "state-borders")
.attr("d", path(topojson.mesh(us, us.objects.states, function (a, b) { return a !== b; })));
});
svg.selectAll("myCircles")
.data(data)
.enter()
.append("circle")
.attr("cx", function (d) { return projection([d.lon, d.lat])[0]; })
.attr("cy", function (d) { return projection([d.lon, d.lat])[1]; })
.attr("r", 14) //first testing with fixed radius and then will scale acccording to count
.style("fill", "69b3a2")
.attr("stroke", "#69b3a2")
.attr("stroke-width", 3)
.attr("fill-opacity", 1);
}
I have no idea whether these bubbles are dropping at the actual place - which I am definitely looking for.
As far as a testing method to see if features are alinging properly, try placing easy to identify landmarks, I use Seatle and Miami below - they're on opposite sides of the area of interest, and it should be easy to tell if they are in the wrong place (in the water or inland).
I'm not sure where they are supposed to fall as I do not have the coordinates but I can tell you they aren't where they are supposed to be.
The reason I can know this is because you are using two different projections for your data.
Mercator Projection
You define one of the projections and use it to position the dots:
var projection = d3.geoMercator()
.scale(200)
.translate([margin.left + width / 2, margin.top + height / 2])
This is a Mercator projection centred at [0°,0°] (by default). Here is the world projected with that projection (with the margin and same sized SVG):
D3 GᴇᴏMᴇʀᴄᴀᴛᴏʀ Wɪᴛʜ Cᴇɴᴛᴇʀ [0,0] ᴀɴᴅ Sᴄᴀʟᴇ 200
You are projecting coordinates for the circles based on this projection.
For reproducability, here's a snippet - you should view in full screen:
var margin = { top: 20, right: 20, bottom: 30, left: 50 },
width = 1040 - margin.left - margin.right,
height = 700 - margin.top - margin.bottom;
d3.json("https://cdn.jsdelivr.net/npm/world-atlas#2/land-50m.json").then(function(json) {
var svg = d3.select("body")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
var projection = d3.geoMercator()
.scale(200)
.translate([margin.left + width / 2, margin.top + height / 2])
var path = d3.geoPath().projection(projection);
svg.append("g")
.attr("class", "states")
.attr("fill-opacity", 0.4)
.selectAll("path")
.data(topojson.feature(json, json.objects.land).features)
.enter().append("path")
.attr("d", path);
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/topojson/3.0.2/topojson.js"></script>
A mystery projection
The second projection is not obvious. If you look at the snippet used to create the above image, you'll notice that it assigns the projection to the path:
var path = d3.geoPath().projection(projection);
This is so the path converts each geographic coordinate (a spherical latitude/longitude pair) to the correct coordinate on the screen (a Cartesian pixel x,y value): a coordinate of [-17°,85°] will be converted to something like [100px,50px].
In your question you simply use:
var path = d3.geoPath();
You don't assign a projection to the path - so d3.geoPath() simply plots every vertice/point in the geojson/topojson as though the coordinate contains pixel coordinates: a coordinate of [100px,50px] in the geojson/topojson is plotted on the SVG at x=100, y=50.
Despite not using a projection, your the US states plot as expected. Why? Because the geojson/topojson was already projected. Since it was preprojected, we don't need to use a projection when we plot it with D3.
Pre-projected geometry can be useful as it requires less calculations to draw, resulting in faster rendering speeds, but comes at a cost of less flexibility (see here).
If we overlay your pre-projected geometry with the geometry you project with d3.geoProjection, we get:
Naturally, you can see there is no point that is the same between the two. Consequently, you are not projecting points so that they properly overlay the pre-projected geometries.
Cᴏᴍᴘᴀʀɪsᴏɴ ʙᴇᴛᴡᴇᴇɴ ᴛʜᴇ ᴛᴡᴏ ᴘʀᴏᴊᴇᴄᴛɪᴏɴs
Snippet to reproduce:
var margin = { top: 20, right: 20, bottom: 30, left: 50 },
width = 1040 - margin.left - margin.right,
height = 700 - margin.top - margin.bottom;
var svg = d3.select("body")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
d3.json("https://cdn.jsdelivr.net/npm/world-atlas#2/land-50m.json").then(function(json) {
var projection = d3.geoMercator()
.scale(200)
.translate([margin.left + width / 2, margin.top + height / 2])
var path = d3.geoPath().projection(projection);
svg.append("g")
.attr("fill-opacity", 0.4)
.selectAll("path")
.data(topojson.feature(json, json.objects.land).features)
.enter().append("path")
.attr("d", path);
})
d3.json("https://d3js.org/us-10m.v1.json").then(function(us) {
var path = d3.geoPath();
svg.append("g")
.attr("fill-opacity", 0.4)
.selectAll("path")
.data(topojson.feature(us, us.objects.states).features)
.enter().append("path")
.attr("d", path);
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/topojson/3.0.2/topojson.js"></script>
Unsatisfactory Solution
Without metadata explaining what projection and coordinate system a geojson/topojson uses, we generally cannot duplicate that projection to overlay other features.
In this case, however, if we look carefully at the plotted US states, we can see that an Albers projection was used to pre-project the state outlines.
Sometimes, we can guess the projection parameters. As I'm fairly familiar with this file (), I can tell you it uses the following parameters:
d3.geoAlbersUsa()
.scale(d3.geoAlbersUsa().scale()*6/5)
.translate([480,300]);
Here's an example showing Miami and Seattle overlain:
var width = 960,height = 600;
var svg = d3.select("body")
.append("svg")
.attr("width",width)
.attr("height",height);
d3.json("https://d3js.org/us-10m.v1.json").then(function(us) {
var path = d3.geoPath();
var projection = d3.geoAlbersUsa()
.scale(d3.geoAlbersUsa().scale()*6/5)
.translate([width/2,height/2]);
svg.append("g")
.attr("fill-opacity", 0.4)
.selectAll("path")
.data(topojson.feature(us, us.objects.states).features)
.enter().append("path")
.attr("d", path);
var places = [
[-122.3367534,47.5996582],
[-80.1942949,25.7645783]
]
svg.selectAll(null)
.data(places)
.enter()
.append("circle")
.attr("r", 3)
.attr("transform", function(d) {
return "translate("+projection(d)+")";
})
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/topojson/3.0.2/topojson.js"></script>
But, this has a downside of being very obtuse in adopting for other screen sizes, translations, centers, scales, etc. Pre-projected geometry also generates a lot of confusion when combined with unprojected geometry. For example, this question shows a common frustration on sizing and centring pre-projected geometry properly.
Better Solution
A better solution is to use one projection for everything. Either pre-project everything first (which is a bit more complex), or project everything on the fly (it really doesn't take that long for a browser). This is just clearer and easier when modifying the visualization or the geographic data.
To project everything the same way, you'll need to make sure all your data is unprojected, that is to say it uses lat/long pairs for its coordinates / coordinate space. As your US json is pre-projected, we'll need to find another, perhaps:
https://bl.ocks.org/mbostock/raw/4090846/us.json
And we simply run everything through the projection:
Snippet won't load the resource, but here's a bl.ock, with the code shown below:
var width =960,height = 600;
var svg = d3.select("body")
.append("svg")
.attr("width",width)
.attr("height",height);
d3.json("us.json").then(function(us) {
var projection = d3.geoAlbersUsa()
.scale(150)
.translate([width/2,height/2]);
var path = d3.geoPath().projection(projection);
svg.append("g")
.attr("fill-opacity", 0.4)
.selectAll("path")
.data(topojson.feature(us, us.objects.states).features)
.enter().append("path")
.attr("d", path);
var places = [
[-122.3367534,47.5996582],
[-80.1942949,25.7645783]
]
svg.selectAll(null)
.data(places)
.enter()
.append("circle")
.attr("r", 3)
.attr("transform", function(d) {
return "translate("+projection(d)+")";
})
})
I define a zoom function:
var zoom = d3.zoom().on("zoom", function () {
svg.attr('transform', d3.event.transform);
});
and call it on this svg variable:
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.call(zoom)
.append("g")
.attr("transform", "translate("
+ width/10 + "," + height/2 + ")");
(where width and height happen to be the size of the screen).
This works great, except for the first time the user zooms. The zoom state is still at the origin, as opposed to the width/10 and height/2 translation.
How do I change the zoom state programmatically to fix this?
Just after writing this I found a very helpful answer here
What worked for me was this:
d3.select('svg').call(zoom.translateBy, width/10, height/2);
I want zoom + brush on a bar chart. I was able to implement zooming ok: demo here, but I found out that after zooming, when you mouse down to drag, the bar chart moves. I don't want the bar chart to move on mouse drag. I want the bar chart to stay in its original place and I want to use the mouse drag for brushing. The bar chart could probably scroll left and right, but it should not be dragged left and right.
How do I make the bar chart stay in its original place?
My code snippet is below. Full script here (134 lines)
var zoom = d3.behavior.zoom()
.x(x)
.scaleExtent([0.8, 10])
.on("zoom", zoomed);
function zoomed() {
// scale x axis
svg.select(".x.axis").call(xAxis);
// scale the bars
var width_scaled = columnwidth * d3.event.scale;
svg.selectAll(".bargroup rect")
.attr("x", function(d) {return x(d.date) - width_scaled/2;})
.attr("width", width_scaled);
}
var brush = d3.svg.brush()
.x(x)
.on("brush", brushed);
function brushed() {
//console.log(brush.extent());
}
var svg = d3.select("#timeline").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.call(zoom);
svg.append("g")
.attr("class", "x brush")
.call(brush)
.selectAll("rect")
.attr("y", -6)
.attr("height", height + 7);
I am displaying different radius circles with different color.
I am trying to place the text(radius value) below each circle but not getting displayed though i could see the text in the browser inspector.
following is the code:
var width=960,height=500;
var margin = {top: 29.5, right: 29.5, bottom: 29.5, left: 59.5};
radiusScale = d3.scale.sqrt().domain([1, 100]).range([10, 39]),
colorScale = d3.scale.category10();
// Create the SVG container and set the origin.
var svg = d3.select("#chart").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var i =0;
while (i<=50)
{
console.log("i value is " + i + " value of scale " +i+ " is " + radiusScale(i));
var circle = svg.append("g").append("circle")
.attr("id","circle" + i)
.attr("cx", i*12 )
.attr("cy", 20)
.attr("fill",colorScale(i))
.attr("r", radiusScale(i))
.append("text").attr("dx",i*12).text(function(d){return radiusScale(i)});
i=i+10;
}
should i be adding the text in svg instead of circle to display below the corresponding circles.
SVG will not display text appended to circle elements. You append to the g element:
var g = svg.append("g");
g.append("circle")
.attr("id","circle" + i)
.attr("cx", i*12 )
.attr("cy", 20)
.attr("fill",colorScale(i))
.attr("r", radiusScale(i));
g.append("text").attr("dx",i*12).text(function(d){return radiusScale(i)});
Also note that your function(d) in .text() isn't necessary, you can do just
g.append("text").attr("dx",i*12).text(radiusScale(i));
I am trying to use the hexbin layout with data that is normally distributed around 0 - all the examples use data centered around the center of the screen, so the scales are the same as the screen scales (except for y inversion)
I've tried to modify the scale functions to account for possible negative values. It works for the y-scale, but the x-scale gives NaNs, and the hexagons are plotted off the screen upper left. That is not the only problem - I would like to programmatically determine the bin size for the hexbin function - in my data series, all of the values are 'binned' into only one to three hexagons, and I need them spread out over the available domain.. here is my code
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://d3js.org/d3.hexbin.v0.min.js?5c6e4f0"></script>
<script>
minMultArray =function(arr,index){
var min = arr[0][index];
for (i=0;i<arr.length;i++) {
min = (min>arr[i][index]?arr[i][index]:min);
}
return min;
};
maxMultArray =function(arr,index){
var max = arr[0][index];
for (i=0;i<arr.length;i++) {
max = (max< arr[i][index]?arr[i][index]:max);
}
return max;
};
var margin = {top: 20, right: 20, bottom: 30, left: 40},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var randomX = d3.random.normal(0, 5),
randomY = d3.random.normal(0, 6),
points = d3.range(2000).map(function() { return [randomX(), randomY()]; });
var minX = minMultArray(points,0);
var minY = minMultArray(points,1);
//var minZ = minMultArray(points,2);
var maxX = maxMultArray(points,0);
var maxY = maxMultArray(points,1);
//var maxZ = maxMultArray(points,2);
var color = d3.scale.linear()
.domain([0, 20])
.range(["white", "steelblue"])
.interpolate(d3.interpolateLab);
var hexbin = d3.hexbin()
.size([width, height])
.radius(20);
alert('minX='+minX +' maxX='+maxX);
var x = d3.scale.linear()
.domain([minX, maxX])
.range(0,width);
alert('xScale(3)='+x(3));
var y = d3.scale.linear()
.domain([minY, maxY])
.range([height, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom")
.tickSize(6, -height);
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.tickSize(6, -width);
console.log('hex = ' +hexbin(points));
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
svg.append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("class", "mesh")
.attr("width", width)
.attr("height", height);
svg.append("g")
.attr("clip-path", "url(#clip)")
.selectAll(".hexagon")
.data(hexbin(points))
.enter().append("path")
.attr("class", "hexagon")
.attr("d", hexbin.hexagon())
.attr("transform", function(d) { return "translate(" + (d.x) + "," + (d.y) + ")"; })
.style("fill", function(d) { return color(d.length); });
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
</script>
After more debugging the hexbin functions, they are not compatible with negative and/or fractional domains- so I solved this by mapping my original data by linear scales up to the height and width of the hexagon plots. Then bin size is controlled by radius. I also modified the hexbin binning function to handle three element arrays, and can compute stats on the third element, using color or size to show mean/median/stddev/max/min. If interested, I can post the code on github...