I have succesfully drawn a map and plotted points from a csv. file on it.
But when I try to add a brush (which should color the circles within the brush in the original color, and the ones outside should have a lower opacity - and when releasing the brush all circles should again have the same color), something goes wrong - The map is shown very quickly and then the entire svg just turns into a single color.
I am pretty new to d3 and have just tried to follow this example: http://bl.ocks.org/feyderm/6bdbc74236c27a843db633981ad22c1b . I can't really figure out if it might have something to do with the projection or something totally different..
My attempt is shown below:
<!DOCTYPE html>
...
<style type="text/css">
.brushed {
fill: white;
stroke: black;
stroke-width: 0.5;
opacity: 0.95;
}
.non_brushed {
fill: grey;
opacity: 0.15;
}
</style>
</head>
<body>
<script type="text/javascript">
//Width and height
var w = 500;
var h = 500;
var padding = 60;
//Define path generator, using the mercator projection
var projection = d3.geoMercator()
.scale(90*w)
.translate([58350, 35330]);
var path = d3.geoPath()
.projection(projection);
//define borough colors
var color = ["rgb(0,59,86)","rgb(63,72,77)",
"rgb(243,142,50)", "rgb(246,99,36)", "rgb(21,108,108)"];
//Create SVG element
var svg_map = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
//Load in GeoJSON data
d3.json("boroughs.json", function(json) {
//Bind data and create one path per GeoJSON feature
svg_map.selectAll("path")
.data(json.features)
.enter()
.append("path")
.attr("d", path)
.style("stroke","white")
.style("stroke-width","1px")
.style("fill",function(d,i){
return color[i];
});
//load in csv data
d3.csv("blabla.csv",function(data){
//create circle elements
var circles = svg_map.append("g")
.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("class","brushed") //original color
.attr("cx", function(d){
return projection([d.Lon,d.Lat])[0];
})
.attr("cy", function(d){
return projection([d.Lon,d.Lat])[1];
})
.attr("r",3);
//create brush
var brush = d3.brush()
.on("brush", highlightBrushedCircles)
.on("end", brushEnd);
svg_map.append("g")
.call(brush);
function highlightBrushedCircles() {
if (d3.event.selection != null) {
// set circles to "non_brushed"
circles.attr("class", "non_brushed");
//coordinates describing the corners of the brush
var brush_coords = d3.brushSelection(this);
// set the circles within the brush to class "brushed" to style them accordingly
circles.filter(function (){
var cx = d3.select(this).attr("cx"),
cy = d3.select(this).attr("cy");
return isBrushed(brush_coords, cx, cy);
})
.attr("class", "brushed");
}
}
function isBrushed(brush_coords, cx, cy) {
//the corners of the brush
var x0 = brush_coords[0][0],
x1 = brush_coords[1][0],
y0 = brush_coords[0][1],
y1 = brush_coords[1][1];
//checks whether the circle is within the brush
return x0 <= cx && cx <= x1 && y0 <= cy && cy <= y1;
}
function brushEnd() {
if (!d3.event.selection) return;
// programmed clearing of brush after mouse-up
d3.select(this).call(brush.move, null);
//set all circles to original color
svg_map.selectAll(".non_brushed").classed("brushed", true);
}
});
});
</script>
</body>
I got it to work - it seemed that the problem was that I in my style css file also had this:
rect {
fill: rgb(21,108,108);
shape-rendering: CrispEdges;
}
Which then just colored the entire svg like that :)
Related
I am pretty new to D3 but i have a basic knowledge of HTML, CSS and JavaScript. And i was tasked to create a dashboard for our team in MS Sharepoint (used to be in Excel lol).
Right now, i only need 6 Arcs to be visually presented in the site page. I can just extract the data lists from sharepoint and compute it via javascript then store it in a variable to throw it in the D3 arc. Here is my current code:
<body>
<div class="container" id="graph_container1">
<svg id="svg1"></svg>
<svg id="svg2"></svg>
<svg id="svg3"></svg>
</div>
<script>
var canvas = d3.select("#svg1")
.attr("width", 400)
.attr("height", 400);
var group = canvas.append("g")
.attr("transform", "translate(150, 150)");
var r = 100;
var p = Math.PI * 2;
var score = 70;
var finalScore = p * (70/100);
var arc = d3.arc()
.innerRadius(r)
.outerRadius(80)
.startAngle(0)
.endAngle(finalScore);
group.append("path").attr("d", arc)
.attr("fill", "orange")
.transition()
.ease(d3.easeLinear)
.duration(2000)
.attrTween("d", pieTween);
function pieTween(b) {
b.innerRadius = 0;
var i = d3.interpolate( {startAngle: 0, endAngle: 0}, b );
return function(t) { return arc(i(t));};
}
</script>
</body>
I have 3 main problems right now:
Putting the score text in the middle of the Arc graph
Making a simple animation for the Arc graph like filling up the graph until to the final score
Creating 5 more graphs just like those
I am copying this animation: https://www.youtube.com/watch?v=kK5kKA-0PUQ. I tried its code but it's not working.
Since in a pie/donut chart there is normally one group translated to the center of the chart (which is the case here), just append a text using text-anchor as middle (here, using just 2 decimal places):
group.append("text")
.attr("text-anchor", "middle")
.text(d3.format(".2f")(finalScore))
Your pieTween function has a parameter (b), but there is no argument being passed, since there is no data bound. Besides that, the arc generator has finalScore as the end angle, and because of that no transition is possible.
Change the arc generator and the pieTween function accordingly:
var arc = d3.arc()
.innerRadius(r)
.outerRadius(80)
.startAngle(0);
function pieTween() {
var i = d3.interpolate({
endAngle: 0
}, {
endAngle: finalScore
});
return function(t) {
return arc(i(t));
};
}
Too broad for S.O., sounds like a request. Try it yourself and, if you can't, ask another question (sharing the non-working code).
Here is the code with those changes:
<body>
<div class="container" id="graph_container1">
<svg id="svg1"></svg>
<svg id="svg2"></svg>
<svg id="svg3"></svg>
</div>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script>
var canvas = d3.select("#svg1")
.attr("width", 400)
.attr("height", 400);
var group = canvas.append("g")
.attr("transform", "translate(150, 150)");
var r = 100;
var p = Math.PI * 2;
var score = 70;
var finalScore = p * (70 / 100);
var arc = d3.arc()
.innerRadius(r)
.outerRadius(80)
.startAngle(0);
group.append("path")
.attr("fill", "orange")
.transition()
.ease(d3.easeLinear)
.duration(2000)
.attrTween("d", pieTween);
group.append("text")
.attr("text-anchor", "middle")
.text(d3.format(".2f")(finalScore))
function pieTween() {
var i = d3.interpolate({
endAngle: 0
}, {
endAngle: finalScore
});
return function(t) {
return arc(i(t));
};
}
</script>
</body>
I've created a globe which has circles and a drag. The problem is that the circles appear on the far side of the globe. I would like those circles to be hidden.
My bl.ock can be found here:
http://bl.ocks.org/anonymous/dc2d4fc810550586d40d4b1ce9088422/40c6e199a5be4e152c0bd94a13ea94eba41f004b
For example, I would like my globe to function like this one: https://bl.ocks.org/larsvers/f8efeabf480244d59001310f70815b4e
I've seen solutions such as this one: How to move points in an orthogonal map? but it doesn't quite work for me. The points simply disappear, as d[0] and d[1] seem to be undefined.
I've also tried using methods such as this: http://blockbuilder.org/tlfrd/df1f1f705c7940a6a7c0dca47041fec8 but that also doesn't seem to work. The problem here seems to be that he is using the json as his data, while my circles data are independent of the json.
Only similar example I've found is the one: https://bl.ocks.org/curran/115407b42ef85b0758595d05c825b346 from Curran but I don't really understand his code. His method is quite different than mine.
Here is my JavaScript code:
(function(){
var h = 600;
var w = 900;
var i = 0;
var map = void 0;
var world = void 0;
var US = void 0;
var margin = {
top: 10,
bottom: 40,
left: 0,
right: 30
};
var circleScale = d3.scaleSqrt()
.domain([0, 4445])
.range([0.5, 10])
var width = w - margin.left - margin.right;
var height = h - margin.top - margin.bottom;
var dragging = function(d){
var c = projection.rotate();
projection.rotate([c[0] + d3.event.dx/6, c[1] - d3.event.dy/6])
map.selectAll('path').attr('d', path);
map.selectAll(".circles").attr("cx", function(d){
var coords = projection([d.Longitude_imp, d.Latitude_imp])
return coords[0];
})
.attr("cy", function(d){
var coords = projection([d.Longitude_imp, d.Latitude_imp])
return coords[1];
})
}
var drag = d3.drag()
.on("drag", dragging)
var projection = d3.geoOrthographic().clipAngle(90);
var path = d3.geoPath().projection(projection);
var svg = d3.select("body")
.append("svg")
.attr("id", "chart")
.attr("width", w)
.attr("height", h)
d3.json("world.json", function(json){
d3.csv("arms_transfer_2012_2016_top - arms_transfer_2012_2016_top.csv", function(error, data){
var countries = topojson.feature(json, json.objects.countries).features
var US = countries[168]
map = svg.append('g').attr('class', 'boundary');
world = map.selectAll('path').data(countries);
US = map.selectAll('.US').data([US]);
Circles = map.selectAll(".circles").data(data)
console.log(countries[168])
world.enter()
.append("path")
.attr("class", "boundary")
.attr("d", path)
US.enter()
.append("path")
.attr("class", "US")
.attr("d", path)
.style("fill", "lightyellow")
.style("stroke", "orange")
Circles.enter()
.append("circle")
.attr("class", "circles")
.attr("r", function(d){
return circleScale(d.Millions)
})
.attr("cx", function(d){
var coords = projection([d.Longitude_imp, d.Latitude_imp])
return coords[0];
})
.attr("cy", function(d){
var coords = projection([d.Longitude_imp, d.Latitude_imp])
return coords[1];
})
.style("fill", "#cd0d0e")
svg.append("rect")
.attr("class", "overlay")
.attr("width", w)
.attr("height", h)
.call(drag)
})
})
})();
There are a few different methods to achieve this, but one of the easier methods would be to calculate the angular distance between the projection centroid (as determined by the rotation) and the circle center on the drag event:
map.selectAll("circle")
.style("display", function(d) {
var circle = [d.Longitude_imp, d.Latitude_imp];
var rotate = projection.rotate(); // antipode of actual rotational center.
var center = [-rotate[0], -rotate[1]]
var distance = d3.geoDistance(circle,center);
return (distance > Math.PI/2 ) ? 'none' : 'inline';
})
Take the center of each point and get the rotational center with projection.rotate() - note that the rotation values are inverse of the centering point. A rotation of [10,-20] centers the map at [-10,20], you move the map under you. With these two points we can use d3.geoDistance() which calculates the distance between two points in radians, hence the use of Math.PI/2 - which gives us points outside of 90 degrees, for these we hide, for the rest we show.
This can be incorporated a little nicer into your code, but I keep it separate here to show what is happening clearer.
Here's an example block - drag to trigger, I haven't applied the logic to the initial load.
An alternative approach, as noted by Gerardo Furtado, would be to use a path to display the circles - using path.pointRadius to set the size of the circle for each point. Instead of appending a circle, you could append path with the following format:
Circles.enter()
.append("path")
.attr("class", "circles")
.attr("d",createGeojsonPoint)
The, on update/drag:
map.selectAll('.circles').attr('d',createGeojsonPoint);
This method uses the clip angle of the orthographic to hide features when they are more than 90 degrees from the center of the projection (as determined by rotation). Your createGeojsonPoint function needs to set the radius and return a valid geojson object:
var createGeojsonPoint = function(d) {
console.log(d);
path.pointRadius(circleScale(d.Millions)); // set point radius
return path({"type":"Point","coordinates":[d.Longitude_imp,d.Latitude_imp]}) // create geojson point, return path data
}
All together, with the necessary modifications, your code might look like this.
I have the following D3.js project that is available here:
http://bl.ocks.org/diggetybo/raw/e75dcb649ae3b26e2312a63434fc970c/
The latitude and longitude inputs are below the map.
It's supposed to take user input numbers of latitude and longitude and "project" svg circles at the given coordinate. The issue is I'm either getting ____ is not a function error or dev tools throws no errors at all, but the circles are never projected.
It's a short file, can someone explain why it's not working the way I thought?
Your update function doesn't make any sense.
It accepts two inputs, but you only ever call it with one.
.selectAll("circle").enter() is not valid d3 syntax.
You need to call projection with both the latitude and longitude, you pass 0 which will result in it returning null since it's outside of the projection.
After you fix all this, you'll still be off because you've moved your paths by your margin and would have been better off putting them in a g moved by the margins.
All that said, a simple rewrite would be:
var lat = d3.select("#latValue").on("input", function() {
update();
}).node();
var long = d3.select("#lonValue").on("input", function() {
update();
}).node();
function update() {
// lat/long to pixel
var coors = projection([long.value, lat.value]);
// if outside projection don't add circle
if (coors === null) return;
// add circle
container
.append("circle")
.attr("cx", coors[0])
.attr("cy", coors[1])
.attr("r", Math.sqrt(5) * 4)
.style("fill", "black")
.style("opacity", 0.85);
}
Running code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<script src="http://d3js.org/d3.v3.min.js"></script>
<style type="text/css">
/* On mouse hover, lighten state color */
path:hover {
fill-opacity: .7;
}
</style>
</head>
<body>
<script type="text/javascript">
//Width and height of map
var width = 960;
var height = 500;
var margins = { left: 0, top: 100, right: 0, bottom: 0 };
// D3 Projection
var projection = d3.geo.albersUsa()
.translate([width/2, height/2]) // translate to center of screen
.scale([1000]); // scale things down so see entire US
// Define path generator
var path = d3.geo.path() // path generator that will convert GeoJSON to SVG paths
.projection(projection); // tell path generator to use albersUsa projection
// Define linear scale for output
var color = d3.scale.linear()
.range(["#c3e2ff","#15198e"]);
//Create SVG element and append map to the SVG
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height+margins.top);
svg.append('text')
.text('Coordinate Query')
.attr('font-size','24px')
.attr('transform', 'translate(' + 30 + ',' +70 + ')')
.attr('font-family','Calibri');
svg.append('text')
.text('Data as of 12/2016')
.attr('font-size','12px')
.attr('transform', 'translate(' + 35 + ',' +100 + ')')
.attr('font-family','Calibri');
// Load in my states data!
color.domain([0,100]); // setting the range of the input data
// Load GeoJSON data and merge with states data
d3.json("https://jsonblob.com/api/573228c3-d068-11e6-b16a-b501dc8d2b08", function(json) {
//var coordinates = d3.mouse(this);
// Bind the data to the SVG and create one path per GeoJSON feature
var container = svg.append("g")
.attr('transform', 'translate(' + margins.left + ',' + margins.top + ')');
container.selectAll("path")
.data(json.features)
.enter()
.append("path")
.attr("d", path)
.style("stroke", "#fff")
.style("stroke-linejoin","round")
.style("stroke-width", "1.5")
.style("fill", 'steelblue');
// Modified Legend Code from Mike Bostock: http://bl.ocks.org/mbostock/3888852
var lat = d3.select("#latValue").on("input", function() {
update();
}).node();
var long = d3.select("#lonValue").on("input", function() {
update();
}).node();
function update() {
// lat/long to pixel
var coors = projection([long.value, lat.value]);
// if outside projection don't add circle
if (coors === null) return;
// add circle
container
.append("circle")
.attr("cx", coors[0])
.attr("cy", coors[1])
.attr("r", Math.sqrt(5) * 4)
.style("fill", "black")
.style("opacity", 0.85);
}
});
</script>
<p>
<label for="latValue"
style="display: inline-block;width:240px;text-align:right;font-size:18px;font-family:Play">
Lattitude:<span id="latValue-value"></span>
</label>
<input type="number"min="-360"max="360"step="1"value="0" id="latValue">
<label for="lonValue"
style="display: inline-block;width:240px;text-align:right;font-size:18px;font-family:Play">
Longitude:<span id="lonValue-value"></span>
</label>
<input type="number"min="-360"max="360"step="1"value="0" id="lonValue">
</p>
</body>
</html>
I am trying to plot a png map file with graticules. The graticule extent should correspond to the width and height of the map file (see end of script). Although the left and upper extent show correctly, the lower and right extent do not correspond to the map dimensions.
I also played around with the extent values in the map function and only the left and upper extent are responsive.
Any suggestions?
<!doctype html>
<meta charset="utf-8">
<script src="./js/d3.js"></script>
<script src="./js/topojson.js"></script>
<script src="./js/jquery3.1.0.min.js"></script>
<style>
.MapPad {
padding: 30px 30px 30px 30px;
}
.graticule {
fill: none;
stroke: #000;
stroke-opacity: .15;
}
.graticule.outline {
stroke: black;
stroke-opacity: 1;
stroke-width: 2px;
stroke-dasharray: initial;
}
.LonLatLabel {
font-family: helvetica;
font-size: 22px;
dominant-baseline: central;
text-anchor: middle;
</style>
<body>
<div id='cont1_1'></div>
<script charset="utf-8">
//The function to plot the maps
function plotMaps (container, width, height, rasterBounds, demFile){
var projection = d3.geoMercator()
.scale(1)
.translate([0, 0]);
var b = [projection(rasterBounds[0]), projection(rasterBounds[1])],
s = 1 / Math.max((b[1][0] - b[0][0]) / width, (b[1][1] - b[0][1]) / height),
t = [(width - s * (b[1][0] + b[0][0])) / 2, (height - s * (b[1][1] + b[0][1])) / 2]
//update projection
projection
.scale(s)
.translate(t)
// geo path generator
var path = d3.geoPath()
.projection(projection)
var map = d3.select(container).append('svg')
.attr('width', width)
.attr('height', height)
.attr('class', 'MapPad');
//define the data layers before drawing to ensure the order of appearance
var gratLines = map.append('g');
var demLayer = map.append('g');
var samplPointsLayer = map.append('g');
var outline = map.append('g');
//make the graticule
var graticule = d3.geoGraticule().extent([[rasterBounds[0][0], rasterBounds[1][0]], [rasterBounds[0][1], rasterBounds[1][1]]]).step([1, 1]);
gratLines.append("path")
.datum(graticule)
.attr("class", "graticule")
.attr("d", path);
// get the coordinates of the line paths and use them as labels
map.selectAll('text')
.data(graticule.lines())
.enter().append("text")
.text(function(d) {
if (d.coordinates[0][0] == d.coordinates[1][0]) {return (d.coordinates[0][0]);}
else if (d.coordinates[0][1] == d.coordinates[1][1]) {return (d.coordinates[0][1]);}
})
.attr("class","LonLatLabel")
.attr('transform', function(d) { return ('translate(' + projection(d.coordinates[0])[0] + ',' + projection(d.coordinates[1])[1] + ')')
});
//outline of the map
outline.append("path")
.datum(graticule.outline)
.attr("class", "graticule outline")
.attr("d", path);
/*var color = d3.scale.ordinal()
.domain(["1", "2", "3"])
.range(["#ffd633", "#aaff00" , "#267300"]);
*/
demLayer.append('svg:image')
.attr('xlink:href', demFile)
.attr('width', width)
.attr('height', height);
d3.json('SamplingPoints.json', function(err, data) {
samplPointsLayer.selectAll('circles')
.data(data.features)
.enter().append('circle')
.attr('r', 5)
.each(function(d) {
var lonlat = projection(d.geometry.coordinates);
d3.select(this)
.attr('cx', lonlat[0])
.attr('cy', lonlat[1])
.style('fill', 'black')
.style("opacity", .5)
});
});
}
//calculate the number with which the size of each map should be divided
var mainWidth = 230
//Plot the maps in each div
//Alps
var widthAlps = 4665;
var heightAlps = 3589;
var resCoefAlps = widthAlps/mainWidth
var rasterBoundsAlps = [[ 5.907077970880465 , 45.29815864865324 ] , [ 11.330836684119511 , 48.15780097787413 ]];
plotMaps('#cont1_1', widthAlps/resCoefAlps, heightAlps/resCoefAlps, rasterBoundsAlps, 'dem_alps.png');
</script>
</body>
Here's the result:
enter image description here
Issue solved! The [xmin, ymin], [xmax, ymax] values in the d3.geoGraticule().extent() function should be:
[rasterBounds[0][0], rasterBounds[0][1]], [rasterBounds[1][0], rasterBounds[1][1]]]
I'm trying to combine these two d3 examples:
http://bl.ocks.org/mbostock/4183330
http://bl.ocks.org/mbostock/2206590
I have a sphere with the projection displaying correctly, and the zoom working correctly. All I'm trying to do now is style it.
I got the world tour example working previously, it uses canvas and I was able to give it a shadow to create a glow effect that I really liked.
After merging these two code pieces I'm now using svg elements and I cannot seem to get the glow effect to work.
Here is my code (the fill attribute of the .globe class seems to be working):
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body {
background: #000000;
}
.background {
fill: none;
pointer-events: all;
}
.feature {
fill: #ccc;
cursor: pointer;
}
.feature.active {
fill: #00FF15;
}
.globe
{
fill:#fff;
strokeStyle: #35C441;
lineWidth: 5;
shadowColor: #35C441;
shadowBlur: 40;
shadowOffsetX: 0;
shadowOffsetY: 0;
}
.mesh {
fill: none;
stroke: #fff;
stroke-linecap: round;
stroke-linejoin: round;
}
</style>
<body>
<script src="d3/d3.v3.min.js"></script>
<script src="d3/topojson.v1.min.js"></script>
<script>
var width = 960,
height = 720;
active = d3.select(null);
var globe = {type: "Sphere"};
var projection = d3.geo.orthographic()
.scale(height / 2.1)
.translate([width / 2, height / 2])
.clipAngle(90)
.precision(.5);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
//append a rectange to the svg element. give it the background css style class.
//on click do reset?
svg.append("rect")
.attr("class", "background")
.attr("width", width)
.attr("height", height)
.on("click", reset);
//append "g" to the svg element
var g = svg.append("g")
.style("stroke-width", "1.5px");
var path = d3.geo.path()
.projection(projection)
d3.json("./world-110m.json", function(error, world) {
g.append("path")
.datum(globe)
.attr("class", "globe")
.attr("d", path);
g.selectAll("path")
.data(topojson.feature(world, world.objects.countries).features)
.enter().append("path")
.attr("d", path)
.attr("class", "feature")
.on("click", clicked);
g.append("path")
.datum(topojson.mesh(world, world.objects.countries, function(a, b) { return a !== b; }))
.attr("class", "mesh")
.attr("d", path);
});
function clicked(d) {
if (active.node() === this) return reset();
active.classed("active", false);
active = d3.select(this).classed("active", true);
var bounds = path.bounds(d),
dx = bounds[1][0] - bounds[0][0],
dy = bounds[1][1] - bounds[0][1],
x = (bounds[0][0] + bounds[1][0]) / 2,
y = (bounds[0][1] + bounds[1][1]) / 2,
scale = .9 / Math.max(dx / width, dy / height),
translate = [width / 2 - scale * x, height / 2 - scale * y];
g.transition()
.duration(750)
.style("stroke-width", 1.5 / scale + "px")
.attr("transform", "translate(" + translate + ")scale(" + scale + ")");
}
function reset() {
active.classed("active", false);
active = d3.select(null);
g.transition()
.duration(750)
.style("stroke-width", "1.5px")
.attr("transform", "");
}
</script>
</body>
</html>
If anyone can help that would be great, or if the answer already exists on here could you please point me in the right direction
Thanks!
It would have helped if you included a picture of the effect you want.
That said, your CSS is simply not valid with SVG elements:
The first two have corresponding styles:
.globe {
fill:#fff;
stroke: #35C441;
stroke-width: 5;
}
Shadows, though, are a bit trickier.