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)+")";
})
})
Related
I am trying to essentially merge these two examples
https://www.d3-graph-gallery.com/graph/line_change_data.html
https://www.d3-graph-gallery.com/graph/line_several_group.html
But for the life of me I cannot get it to work. The data is coming in from pandas/flask so I have complete control on how to format it.
I also tried to simply feed in different datasets into the multiline plot (i.e. not worry about updating in a fancy transition, just swap between plots) but it kept just drawing the new lines without removing the old ones (i googled for various .exit() and .remove() strategies, but either it didn't work or removed the whole picture)
I feel the transition might be complicated as my understanding is one needs to give new data to the old points on the axis, but if that line doesn't exist in the new plot what would happen?
EDIT: Okay so I found a way to just switch between plots and refresh things with the following code, in particular by removing all the 'g' elements and then redrawing the axes which are lost in this (as you can tell I am still learning the ropes as to the different components). I tried only removing line and path elements but that did not work, would appreciate input on that too please.
So in this case, how would I go about updating this using transitions?
HTML
<!DOCTYPE html>
<meta charset="utf-8">
<!-- Load d3.js -->
<script src="https://d3js.org/d3.v4.js"></script>
<!-- Add 2 buttons -->
<button onclick="update(data1)">Dataset 1</button>
<button onclick="update(data2)">Dataset 2</button>
<button onclick="update(d_all)">Dataset 3</button>
<!-- Create a div where the graph will take place -->
<div id="my_dataviz"></div>
<script>
var data1 = {{ d1|safe }}
var data2 = {{ d2|safe }}
var d_all = {{ d_all | safe}}
</script>
<script type="text/javascript" src="{{ url_for('static', filename='scripts/test.js') }}"></script>
JS
// set the dimensions and margins of the graph
var margin = {top: 10, right: 30, bottom: 30, left: 50},
width = 460 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;
// append the svg object to the body of the page
var svg = d3.select("#my_dataviz")
.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 + ")");
// Initialise a X axis:
var x = d3.scaleLinear().range([0,width]);
var xAxis = d3.axisBottom().scale(x);
// Initialize an Y axis
var y = d3.scaleLinear().range([height, 0]);
var yAxis = d3.axisLeft().scale(y);
// Create a function that takes a dataset as input and update the plot:
function update(data) {
d3.selectAll("g > *").remove()
svg.append("g")
.attr("class","myYaxis")
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.attr("class","myXaxis")
// Create the X axis:
x.domain([0, d3.max(data, function(d) { return d.ser1 }) ]);
svg.selectAll(".myXaxis") //.transition()
// .duration(3000)
.call(xAxis);
// create the Y axis
y.domain([0, d3.max(data, function(d) { return d.ser2 }) ]);
svg.selectAll(".myYaxis")
// .transition()
// .duration(3000)
.call(yAxis);
var grouped = d3.nest() // nest function allows to group the calculation per level of a factor
.key(function(d) { return d.l;})
.entries(data);
// color palette
var res = grouped.map(function(d){ return d.key }) // list of group names
var color = d3.scaleOrdinal()
.domain(res)
.range(['#e41a1c','#377eb8','#4daf4a','#984ea3','#ff7f00','#ffff33','#a65628','#f781bf','#999999'])
// Draw the line
svg.selectAll(".line")
.data(grouped)
.enter()
.append("path")
.attr("fill", "none")
.attr("stroke", function(d){ return color(d.key) })
.attr("stroke-width", 1.5)
.attr("d", function(d){
return d3.line()
.x(function(d) { return x(d.ser1); })
.y(function(d) { return y(d.ser2); })
(d.values)
})
}
update(d_all)
Okay I more or less have a working answer for this, it's not perfect but it deals with the bulk of it as far as i can tell.
Watching this video helped a lot too so kudos there
https://www.youtube.com/watch?v=IyIAR65G-GQ
// set the dimensions and margins of the graph
var margin = {top: 10, right: 30, bottom: 30, left: 50},
width = 460 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;
// append the svg object to the body of the page
var svg = d3.select("#my_dataviz")
.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 + ")");
// Initialise a X axis:
var x = d3.scaleLinear().range([0,width]);
var xAxis = d3.axisBottom().scale(x);
// Initialize an Y axis
var y = d3.scaleLinear().range([height, 0]);
var yAxis = d3.axisLeft().scale(y);
svg.append("g")
.attr("class","myYaxis")
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.attr("class","myXaxis")
// Create a function that takes a dataset as input and update the plot:
function update(data) {
// Create the X axis:
x.domain([0, d3.max(data, function(d) { return d.ser1 }) ]);
svg.selectAll(".myXaxis") //.transition()
// .duration(3000)
.call(xAxis);
// create the Y axis
y.domain([0, d3.max(data, function(d) { return d.ser2 }) ]);
svg.selectAll(".myYaxis")
// .transition()
// .duration(3000)
.call(yAxis);
var grouped = d3.nest() // nest function allows to group the calculation per level of a factor
.key(function(d) { return d.l;})
.entries(data);
var res = grouped.map(function(d){ return d.key }) // list of group names
var color = d3.scaleOrdinal()
.domain(res)
.range(['#e41a1c','#377eb8','#4daf4a','#984ea3','#ff7f00','#ffff33','#a65628','#f781bf','#999999'])
var update = svg.selectAll(".line")
.data(grouped);
var t01Enter = update.enter().append("g")
.attr("class", ".line");
t01Enter.append("path")
.attr("class", "line")
.attr("fill", "none")
.style("opacity", 0.0)
.merge(update)
.transition()
.duration(2000)
.style("opacity", 1.0)
.attr("d", function(d){
return d3.line()
.x(function(d) { return x(d.ser1); })
.y(function(d) { return y(d.ser2); })
(d.values)
})
.style("stroke", function(d){ return color(d.key) })
.attr("stroke-width", 1.5)
update.exit()
.transition().duration(1000).style("opacity", 0.0).remove()
}
update(d_all)
I'd still like to work out how to chain the transitions more carefully and avoid issues of returning a type transition to merge, but that's for the next post!
Good Image
Bad Image
I am trying to create a D3 graph which looks like the Illustrator created design (Good Image 1), but the closest in terms of positioning I have been able to get is the second (Bad Image 2).
I'm really new to D3 and creating SVGs in general, so I may be going about this all wrong. The code below is what I've been able to figure out / find online. It looks like I can't directly adjust positioning of the elements themselves using css positioning? I tried adding classes via the html and also in JQuery with $(.myClass).css..., but everything I do has exactly zero effect. The only thing that seems to work is transform, but it's ugly, as can be seen in the second pic.
var margin = { left:10, right:10, top:10, bottom:10 };
var width = 400 - margin.left - margin.right,
height = 450 - margin.top - margin.bottom;
var g = d3.select("#pyramid-chart-area")
.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 + ")");
d3.json("../data/pyramid_hp.json").then(function(data){
data.forEach(function(d){
d.hp = +d.hp;
});
var x = d3.scaleBand()
.domain(data.map(function(d){ return d.hp; }))
.range([0, width])
.paddingInner(0.3)
.paddingOuter(0.3);
var y = d3.scaleLinear()
.domain([0, d3.max(data, function(d){
return d.hp;
})])
.range([height, 0]);
var xAxisCall = d3.axisBottom(x);
g.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0, " + height + ")")
.call(xAxisCall)
.selectAll("text")
.attr("y", "10")
.attr("x", "-5")
.attr("text-anchor", "end")
.attr("transform", "rotate(-40)");
var yAxisCall = d3.axisLeft(y)
.ticks(3)
.tickFormat(function(d){
return d;
});
g.append("g")
.attr("class", "y-axis")
.call(yAxisCall);
var arc = d3.symbol().type(d3.symbolTriangle)
.size(function(d){ return scale(d.hp); });
var scale = d3.scaleLinear()
.domain([0, 5])
.range([0, width]);
var colors = d3.scaleLinear()
.domain(d3.extent(data, function(d) {return d.hp}))
.range([
'#ffffff',
'#303030'
]);
var group = g.append('g')
.attr('transform','translate('+ 192 +','+ 320 +')')
.attr('class', 'triangle-container');
var line = group.selectAll('path')
.data(data)
.enter()
.append('path')
.attr('d', arc)
// .attr('fill',function(d){ return colorscale(d.hp); })
.attr('fill', d => colors(d.hp))
.attr('stroke','#000')
.attr('stroke-width', 1)
.attr('class', 'triangle')
.attr('transform',function(d,i){ return "translate("+ (i * 20) +","+(i * 10)+")"; });
You can position the symbols, but its tricky - symbol size represents area and as rioV8 notes symbols are positioned by their center. But if you can figure out the properties of the triangle we can place it relatively easily.
In the case of a equilateral triangle, you'll want to know the length of a given side as well as the height of that centroid (which is triangle height/3). So these functions will likely be useful:
// get length of a side/width of upright equilateral triangle from area:
function getWidth(a) {
return Math.sqrt(4 * a / Math.sqrt(3));
}
// get height of the triangle from length of a side
function getHeight(l) {
return Math.sqrt(3)*l/2;
}
Using the height of the centroid we can position the circle where we want with something like:
y = SVGheight - SymbolHeight/3 - marginBottom;
No need for scaling here.
The x values of each symbol do need some scaling to arrange them to your liking. Below I use a linear scale with a range of [width/10,0] arbitrarily, the denominator will change the horizontal skew in this case, there are probably better ways to fine tune this.
With this you can achieve the desired result:
For simplicity's sake, below I'm using data (since you don't show any) that represents pixel area - scaling must be factored into the height and width calculations if scaling areas. I've also included circles on the top of each triangle for possible label use, since we know the dimensions of the triangle this is trivial now
var margin = { left:10, right:10, top:10, bottom:10 };
var width = 400 - margin.left - margin.right,
height = 300 - margin.top - margin.bottom;
var g = 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 data = [
{a: 40000},
{a: 30000},
{a: 20000},
{a: 10000}
];
function getWidth(a) {
return Math.sqrt(4 * a / Math.sqrt(3));
}
function getHeight(l) {
return Math.sqrt(3)*l/2;
}
data.forEach(function(d) {
d.w = getWidth(d.a);
d.h = getHeight(d.w);
})
var x = d3.scaleLinear()
.domain(d3.extent(data, function(d){ return d.w; }) )
.range([width/10,0]);
var arc = d3.symbol().type(d3.symbolTriangle)
.size(function(d){ return d.a; });
var colors = d3.scaleLinear()
.domain(d3.extent(data, function(d) {return d.a}))
.range(['#ffffff','#303030']);
var group = g.append('g')
.attr('transform','translate('+ width/2 +',0)')
.attr('class', 'triangle-container');
var line = group.selectAll('path')
.data(data)
.enter()
.append('path')
.attr('d', arc)
.attr('fill', d => colors(d.a))
.attr('class', 'triangle')
.attr('transform',function(d,i){ return "translate("+ x(d.w) +","+ (height - d.h/3 - margin.bottom ) +")"; });
var circles = group.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cx", function(d) { return x(d.w); })
.attr("cy", function(d) { return height - d.h - margin.bottom; })
.attr("r", 3);
<script src='https://d3js.org/d3.v5.min.js' type='text/javascript'></script>
axes could present a bit of a challenge
I have a simple d3 area chart, with two areas plotted using the following data:
var data = [
[{'year':0,'amount':2},{'year':1,'amount':3},{'year':2,'amount':9},{'year':3,'amount':5},{'year':4,'amount':6},{'year':5,'amount':7},{'year':6,'amount':8},{'year':7,'amount':9},{'year':8,'amount':10},{'year':9,'amount':11},{'year':10,'amount':12}],
[{'year':0,'amount':1},{'year':1,'amount':2},{'year':2,'amount':8},{'year':3,'amount':4},{'year':4,'amount':5},{'year':5,'amount':6},{'year':6,'amount':7},{'year':7,'amount':8},{'year':8,'amount':9},{'year':9,'amount':10},{'year':10,'amount':11}]
];
The two separate arrays of objects allow me to plot two areas on one chart using the code below:
var colors = [
'steelblue',
'lightblue',
];
var margin = {top: 20, right: 30, bottom: 30, left: 50},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var x = d3.scale.linear()
.domain(d3.extent(data.map(function(d,i) { console.log(d); return d[i].year; })))
.range([0, width]);
var y = d3.scale.linear()
.domain([-1, 16])
.range([height, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.tickSize(-height)
.tickPadding(10)
.tickSubdivide(true)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.tickPadding(10)
.tickSize(-width)
.tickSubdivide(true)
.orient("left");
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("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
svg.append("g")
.attr("class", "y axis")
.append("text")
.attr("class", "axis-label")
.attr("transform", "rotate(-90)")
.attr("y", (-margin.left) + 10)
.attr("x", -height/2)
.text('Axis Label');
svg.append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", height);
//************************************************************
// Create D3 line object and draw data on our SVG object
//************************************************************
var line = d3.svg.area()
.interpolate("cardinal")
.x(function(d) { return x(d.year); })
.y0(height)
.y1(function(d) { return y(d.amount); });
svg.selectAll('.line')
.data(data)
.enter()
.append("path")
.attr("class", "area")
.attr('fill', function(d,i){
return colors[i%colors.length];
})
.attr("d", line);
Thing is I need to set the domains based on the data. I've tried doing:
.domain(d3.extent(data.map(function(d) { return d.amount; })))
...when creating my linear scale but obviously this doesn't work as the array map in the call above just maps out the nested arrays instead of the objects inside.
How do I set the domain using data in this format? Or is there a better way to structure my data whilst still allowing for multiple areas to be drawn?
To get the overall extent of all the amount values contained in both arrays you need to somehow merge these arrays into one. There are several ways this could be done:
d3.merge() to merge both arrays into one:
var allValues = d3.merge(data);
The main advantage of this approach over the following ones is the fact, that this will work with any number of nested arrays in data without any changes to the code.
Built-in method Array.prototype.concat():
var allValues = data[0].concat(data[1])
If you want to show off and don't need to be compatible with older version of JavaScript, you can apply the spread operator new to ES6:
var allValues = [...data[0], ...data[1]];
Having this flattened array containing all values you can pass it to d3.extent() to calculate the overall extent.
var extent = d3.extent(allValues, function(d) { return d.amount; });
var data = [
[{'year':0,'amount':2},{'year':1,'amount':3},{'year':2,'amount':9},{'year':3,'amount':5},{'year':4,'amount':6},{'year':5,'amount':7},{'year':6,'amount':8},{'year':7,'amount':9},{'year':8,'amount':10},{'year':9,'amount':11},{'year':10,'amount':12}],
[{'year':0,'amount':1},{'year':1,'amount':2},{'year':2,'amount':8},{'year':3,'amount':4},{'year':4,'amount':5},{'year':5,'amount':6},{'year':6,'amount':7},{'year':7,'amount':8},{'year':8,'amount':9},{'year':9,'amount':10},{'year':10,'amount':11}]
];
console.log(d3.extent(d3.merge(data), function(d) { return d.amount; })); // using d3.merge()
console.log(d3.extent(data[0].concat(data[1]), function(d) { return d.amount; })); // using Array.prototype.concat()
// This will only work in compatible browsers which support the new ES6 spread operator
console.log(d3.extent([...data[0], ...data[1]], function(d) { return d.amount; })); // using ES6 spread operator
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
I am trying do some path interpolation in D3. I'd like to produce an area plot like this, but I want to transition the area along the y-axis, starting from the bottom of the xaxis up to the final position shown in the example. Here's a quick sketch to explain what I'd like to do:
I'd like to start the transition with no area:
and transition it up along the y-axis:
Using the code, copied from the example, here's what I'd trying to do:
var margin = {top: 20, right: 20, bottom: 30, left: 50},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var parseDate = d3.time.format("%d-%b-%y").parse;
var x = d3.time.scale()
.range([0, width]);
var y = d3.scale.linear()
.range([height, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
var area = d3.svg.area()
.x(function(d) { return x(d.date); })
.y0(height)
.y1(function(d) { return y(d.close); });
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 + ")");
d3.tsv("data.tsv", function(error, data) {
data.forEach(function(d) {
d.date = parseDate(d.date);
d.close = +d.close;
});
x.domain(d3.extent(data, function(d) { return d.date; }));
y.domain([0, d3.max(data, function(d) { return d.close; })]);
var dataSel = svg.selectAll('.area').data(data)
dataSel.exit().remove()
dataSel.enter()
.append('path')
.attr("class", "area")
.attr("d", 'M0,0h' + width) // my idea here was to draw a path that
// has no area along the x-axis and then
// interpolate the path up to the final area
dataSel.transition() // transition the path to its final position
.duration(1000)
.attr("d", area)
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Price ($)");
});
Can anyone explain/show how I can transition a path along one axis as I'm trying to do?
D3 path transitions only really work if the starting and ending paths have the same number of control points. So, for example, D3 can't transition a line into an area. What you could do, however, is something like the following:
Use d3.area to generate the final path for the area.
Make a copy of the path and, in the copy, change all the control points that correspond to the "top" of the area to control points on the "bottom" of the area. (In other words, modify their y-values.)
Draw the area using this modified path.
Transition to the final path.
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...