I'm trying to create a visual effect (using D3.js and json) where 4 small circles will pop up once I click on the big yellow circle. I want to make sure that ALL of the circles will present pieces of information that I'm trying to assign to each and every one of them. I plan to have:
-The 1st big yellow circle to present the weather (sunny, mostly cloudy, etc)
-2 small circles to present weather info by word
-2 small circles present a D3 bar chart that I made, along with some weather info by word
(I have all of the info separated by spaces and comments)
However, my problem is (for the lack of a better explanation) that I am just stumped! I have no idea how to make that happen. I would really appreciate it if I can get help from you guys. Here's my code (HTML and JS):
var h = 2000;
var w = 2000;
var xGrid = 300;
var yGrid = 300;
var radius = 300;
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
var shape = svg.append("circle")
.attr("cx", xGrid)
.attr("cy", yGrid)
.attr("r", radius)
.style("fill", "yellow");
shape.on("click", function(){
var circle1 = svg.append("circle")
//.attr("cx", (xGrid-radius/2)+0)
//.attr("cy", (yGrid-radius/2)+0)
.attr("cx", xGrid - radius/2)
.attr("cy", yGrid - radius/2)
.attr("r", radius/2)
.style("fill", "red");
var circle2 = svg.append("circle")
//.attr("cx", (xGrid-radius/2)+10)
//.attr("cy", (yGrid-radius/2)+10)
.attr("cx", xGrid + radius/2)
.attr("cy", yGrid - radius/2)
.attr("r", radius/2)
.style("fill", "blue");
var circle3 = svg.append("circle")
// .attr("cx", (xGrid-radius/2)+20)
// .attr("cy", (yGrid-radius/2)+20)
.attr("cx", xGrid - radius/2)
.attr("cy", yGrid + radius/2)
.attr("r", radius/2)
.style("fill", "green");
var circle4 = svg.append("circle")
// .attr("cx", (xGrid-radius/2)+30)
//.attr("cy", (yGrid-radius/2)+30)
.attr("cx", xGrid + radius/2)
.attr("cy", yGrid + radius/2)
.attr("r", radius/2)
.style("fill", "purple");
});
<!-- ///////////////////////////////////////////////////////////////////////////// /////////////////
///////////////////////////////(END) Circle Pop-up (END)/////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////-->
</script>
<!--///////////////////////////////////////////////////////////////////////////// ////////////////////////
///////////////////////////(START) Info for circles to present (START)////////////////////////////////
///////////////////////////////////////////////////////////////////////////// //////////////////////////////-->
<!--/////Main Circle display/////////-->
<p id="w"></p><p id="main"></p>
<!--//////Circle 1 (upper left corner) display///////////-->
<p id="rh"></p><p id="c1a"></p>
<!--///////Circle 2 (upper right corner) display//////////-->
<p id="ws"></p><p id="c2a"></p>
<p id="wd"></p><p id="c2b"></p>
<p id="we"></p><p id="c2c"></p>
<p id="wm"></p><p id="c2d"></p>
<!--////////Circle 3 (lower left corner) display/////////-->
<p id="pti"></p><p id="c3a"></p>
<p id="ptc"></p><p id="c3b"></p>
<p id="df"></p><p id="c3c"></p>
<p id="dc"></p><p id="c3d"></p>
<!--///////Circle 4 (lower right corner) display//////////-->
<p id="hif"></p><p id="c4a"></p>
<p id="hic"></p><p id="c4b"></p>
<p id="sr"></p><p id="c4c"></p>
<p id="uv"></p><p id="c4d"></p>
<script type = "text/javascript">
var dataForMainCircle = '{"weather": "Mostly Cloudy"}';
var mcDis= JSON.parse(dataForMainCircle);
var weather = "weather: ";
document.getElementById("w").innerHTML = weather;
document.getElementById("main").innerHTML = mcDis.weather;
/////////////////////////////////////////////////////////////////////////////////
////////////Setup for display of 1st circle info///////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////
var dataForCircle1b = '{"relative_humidity": "92%"}';
var relativeHum = "Relative Humidity: ";
var c1Dis = JSON.parse(dataForCircle1b);
d3.json("js/tempGraph.json", function (data) {
var canvas = d3.select("body").append("svg")
.attr("width", 500)
.attr("height", 500)
canvas.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("width", function (d) {return d.temp * 10; })
.attr("height", 48)
.attr("y", function (d,i) {return i * 50; })
.attr("fill", "red");
canvas.selectAll("text")
.data(data)
.enter()
.append("text")
.attr("fill", "white")
.attr("y", function (d,i) {return i * 50 + 24; })
.text(function (d) {return d.temp; })
})
document.getElementById("rh").innerHTML = relativeHum;
document.getElementById("c1a").innerHTML = c1Dis.relative_humidity;
/////////////////////////////////////////////////////////////////////////////////////////
/ ////////////////////Setup for display of 2nd circle info////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////
var dataForCircle2 = '{"wind_string": "Calm", "wind_dir": "SW", "wind_degrees": 229, "wind_mph": 2}';
var c2Dis = JSON.parse(dataForCircle2);
var windCon = "Wind Condition: ";
var windDir = "Wind Direction: ";
var windDeg = "Wind Degree: ";
var windMph = "Wind (Miles Per Hour): "
document.getElementById("ws").innerHTML = windCon;
document.getElementById("c2a").innerHTML = c2Dis.wind_string;
document.getElementById("wd").innerHTML = windDir;
document.getElementById("c2b").innerHTML = c2Dis.wind_dir;
document.getElementById("we").innerHTML = windDeg;
document.getElementById("c2c").innerHTML = c2Dis.wind_degrees;
document.getElementById("wm").innerHTML = windMph;
document.getElementById("c2d").innerHTML = c2Dis.wind_mph;
///////////////////////////////////////////////////////////////////////////// //////////////////////////////////
//Setup for display of 3rd circle info/////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////// ////////////////////////////////
var dataForCircle3 = '{"precip_today_in": "0.00", "precip_today_metric": "0"}';
var c3Dis = JSON.parse(dataForCircle3);
var predate = "Today's Precipitation: ";
var prem = "Precipitation Metric: ";
//var dewF = "Dewpoint-F: ";
//var dewC = "Dewpoint-C: ";
document.getElementById("pti").innerHTML = predate;
document.getElementById("c3a").innerHTML = c3Dis.precip_today_in;
document.getElementById("ptc").innerHTML = prem;
document.getElementById("c3b").innerHTML = c3Dis.precip_today_metric;
//document.getElementById("df").innerHTML = dewF;
//document.getElementById("c3c").innerHTML = c3Dis.dewpoint_f;
d3.json("js/dewGraph.json", function (data) {
var canvas = d3.select("body").append("svg")
.attr("width", 500)
.attr("height", 500)
canvas.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("width", function (d) {return d.dewpoint * 10; })
.attr("height", 48)
.attr("y", function (d,i) {return i * 50; })
.attr("fill", "white");
canvas.selectAll("text")
.data(data)
.enter()
.append("text")
.attr("fill", "white")
.attr("y", function (d,i) {return i * 50 + 24; })
.text(function (d) {return d.dewpoint; })
})
//document.getElementById("dc").innerHTML = dewC;
//document.getElementById("c3d").innerHTML = c3Dis.dewpoint_c;
<!--//////////////Setup for display of 4th circle display////////////////////-->
var dataForCircle4 = '{"heat_index_f": "NA", "heat_index_c": "NA", "solarradiation": "--", "UV": "0"}';
var c4Dis = JSON.parse(dataForCircle4);
var heatF = "Heat Index-F: ";
var heatC = "Heat Index-C: ";
var sunR = "Solar Radiation: ";
var ultraV = "UV: ";
document.getElementById("hif").innerHTML = heatF;
document.getElementById("c4a").innerHTML = c4Dis.heat_index_f;
document.getElementById("hic").innerHTML = heatC;
document.getElementById("c4b").innerHTML = c4Dis.heat_index_c;
document.getElementById("sr").innerHTML = sunR;
document.getElementById("c4c").innerHTML = c4Dis.solarradiation;
document.getElementById("uv").innerHTML = ultraV;
document.getElementById("c4d").innerHTML = c4Dis.UV;
d3.json("js/tempGraph.json", function (data) {
var canvas1 = d3.select("body").append("svg")
.attr("width", 500)
.attr("height", 500)
canvas1.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("width", function (d) {return d.temp * 10; })
.attr("height", 48)
.attr("y", function (d,i) {return i * 50; })
.attr("fill", "red");
canvas1.selectAll("text")
.data(data)
.enter()
.append("text")
.attr("fill", "white")
.attr("y", function (d,i) {return i * 50 + 24; })
.text(function (d) {return d.temp; })
})
d3.json("js/dewGraph.json", function (data) {
var canvas2 = d3.select("body").append("svg")
.attr("width", 500)
.attr("height", 500)
canvas2.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("width", function (d) {return d.dewpoint * 10; })
.attr("height", 48)
.attr("y", function (d,i) {return i * 50; })
.attr("fill", "blue");
canvas2.selectAll("text")
.data(data)
.enter()
.append("text")
.attr("fill", "white")
.attr("y", function (d,i) {return i * 50 + 24; })
.text(function (d) {return d.dewpoint; })
})
</script>
</body>
</html>
Keep in mind, this program is sort of a prototype. My main concern is getting the info assigned for the circles INSIDE of them. If you guys found any errors/mix-ups in the code, feel free to notify me. Thank you!!
Ok, as promised, an example. It doesn't look super cool, but at least you will get the picture. If you have more questions, please shoot! Here goes:
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="donut.js" type="text/javascript"></script>
<style>
body{
background-color:black;
}
#container {
position:relative;
display:inline-block;
width:100%;
}
.row{
position:relative;
display:inline-block;
width:100%;
text-align:center;
}
#chart{
width:60%;
position:relative;
display:inline-block;
}
text{
font-size:1.4em;
}
</style>
</head>
<body>
<div id="container">
<div class="row">
<div id="chart">
</div>
</div>
</div>
<script>
var data = [
{"type":"weather", "status":"sunny"},
{"type":"humidity", "status":"high"},
{"type":"temperature", "status":"23°"},
{"type":"pressure", "status":"1040 hpa"},
{"type":"wind", "status":"East"},
];
var chartWidth = 500;
var chartHeight = 500;
/* Adding the svg drawing canvas by selecting the element that will contain the svg. put it in a var,
as it is a d3 selection. We can reuse this selection. Remember, when chaining, you will get returned the
element you last appended, in this case the svg.
*/
var svg = d3.select("#chart")
.append("svg")
.attr({ // you can put attributes in a javascript object, i like this notation better.
width:chartWidth,
height:chartHeight
});
/* Here I am going to add the circles. With SVG elements, you cannot draw inside other elements. There are only 2 elements
which can contain other elements (as far as I know) and that is the SVG element itself and the g element. So if you want
to give the impression that the elements are inside one another, you need to make them overlap. So if I add a circle and then
add text with coordinates that match the circle, the text will overlap the circle, giving the impression it is inside it.
*/
var circles = svg.selectAll("circle")
.data(data) //binding the data. I want 5 circles for my 5 pieces of data
.enter()
.append("circle")
.attr({
cx:function(d,i){ //when doing the append on a d3 selection, you are iteration over each element!
/* I will use the index (i) to 'identify' the pieces of data and to hardcode their x central point positions */
if(i == 0) {
return chartWidth/2;
}
if(i == 1) {
return chartWidth/5;
}
if(i == 2) {
return chartWidth *(2/7);
}
if(i == 3) {
return chartWidth *(5/7);
}
if(i == 4) {
return chartWidth * (4/5);
}
},
cy:function(d,i){ //when doing the append on a d3 selection, you are iteration over each element!
/* I will use the index (i) to 'identify' the pieces of data and to hardcode their y central point positions */
if(i == 0) {
return chartHeight/2;
}
if(i == 1) {
return chartHeight *(3/5);
}
if(i == 2) {
return chartHeight *(4/5);
}
if(i == 3) {
return chartHeight *(4/5);
}
if(i == 4) {
return chartHeight * (3/5);
}
},
r:function(d,i) { /* the radius is in function of the type. The first one (weather) should be the biggest one */
if(d.type === "weather") {
return 200;
}
else{
return 75;
}
},
fill:"yellow",
stroke:"black"
});
/* Now i'll append the text as last, so it overlaps nicely with the circles. For positioning, i ll have to reuse the x and y functions
from the circles. I want the text to be positioned at the center of the cirkels.
*/
var texts = svg.selectAll("text")
.data(data)
.enter()
.append("text")
.text(function(d){ return d.status})
.attr({
x:function(d,i){
/* So I used the same positioning for the text as for the center points of the circles.
you should realize that the text really starts at the middle of the circles. So you
should substract a bit from the x position to get them nicely in the middle. I aint going
for that trouble, its just an example.
*/
if(i == 0) {
return chartWidth/2;
}
if(i == 1) {
return chartWidth/5;
}
if(i == 2) {
return chartWidth *(2/7);
}
if(i == 3) {
return chartWidth *(5/7);
}
if(i == 4) {
return chartWidth * (4/5);
}
},
y:function(d,i){ //when doing the append on a d3 selection, you are iteration over each element!
/* I will use the index (i) to 'identify' the pieces of data and to hardcode their y central point positions */
if(i == 0) {
return chartHeight/2;
}
if(i == 1) {
return chartHeight *(3/5);
}
if(i == 2) {
return chartHeight *(4/5);
}
if(i == 3) {
return chartHeight *(4/5);
}
if(i == 4) {
return chartHeight * (3/5);
}
}
});
</script>
</body>
</html>
I did not add the function to click on the first circle to show the others. I could if you want me to though, but I felt like this example already has enough stuff in it to learn from.
Related
I'm trying to make my map look this way
Unfortunately, my code looks this way, and I don't understand why my text nodes are so gigantic not the way I want it
this is the code that I have going or check my fiddle
This code specifically doesn't seem to produce human readable labels
grp
.append("text")
.attr("fill", "#000")
.style("text-anchor", "middle")
.attr("font-family", "Verdana")
.attr("x", 0)
.attr("y", 0)
.attr("font-size", "10")
.text(function (d, i) {
return name;
});
Here's my full code:
var width = 500,
height = 275,
centered;
var projection = d3.geo
.conicConformal()
.rotate([103, 0])
.center([0, 63])
.parallels([49, 77])
.scale(500)
.translate([width / 2.5, height / 2])
.precision(0.1);
var path = d3.geo.path().projection(projection);
var svg = d3
.select("#map-selector-app")
.append("svg")
.attr("viewBox", `0 0 ${width} ${height}`);
// .attr("width", width)
// .attr("height", height);
svg
.append("rect")
.attr("class", "background-svg-map")
.attr("width", width)
.attr("height", height)
.on("click", clicked);
var g = svg.append("g");
var json = null;
var subregions = {
Western: { centroid: null },
Prairies: { centroid: null },
"Northern Territories": { centroid: null },
Ontario: { centroid: null },
Québec: { centroid: null },
Atlantic: { centroid: null },
};
d3.json(
"https://gist.githubusercontent.com/KatFishSnake/7f3dc88b0a2fa0e8c806111f983dfa60/raw/7fff9e40932feb6c0181b8f3f983edbdc80bf748/canadaprovtopo.json",
function (error, canada) {
if (error) throw error;
json = topojson.feature(canada, canada.objects.canadaprov);
g.append("g")
.attr("id", "provinces")
.selectAll("path")
.data(json.features)
.enter()
.append("path")
.attr("d", path)
.on("click", clicked);
g.append("g")
.attr("id", "province-borders")
.append("path")
.datum(
topojson.mesh(canada, canada.objects.canadaprov, function (
a,
b
) {
return a !== b;
})
)
.attr("id", "province-borders-path")
.attr("d", path);
// g.select("g")
// .selectAll("path")
// .each(function (d, i) {
// var centroid = path.centroid(d);
// });
Object.keys(subregions).forEach((rkey) => {
var p = "";
json.features.forEach(function (f, i) {
if (f.properties.subregion === rkey) {
p += path(f);
}
});
var tmp = svg.append("path").attr("d", p);
subregions[rkey].centroid = getCentroid(tmp.node());
subregions[rkey].name = rkey;
tmp.remove();
});
Object.values(subregions).forEach(({ centroid, name }) => {
var w = 80;
var h = 30;
var grp = g
.append("svg")
// .attr("width", w)
// .attr("height", h)
.attr("viewBox", `0 0 ${w} ${h}`)
.attr("x", centroid[0] - w / 2)
.attr("y", centroid[1] - h / 2);
// grp
// .append("rect")
// .attr("width", 80)
// .attr("height", 27)
// .attr("rx", 10)
// .attr("fill", "rgb(230, 230, 230)")
// .attr("stroke-width", "2")
// .attr("stroke", "#FFF");
grp
.append("text")
.attr("fill", "#000")
.style("text-anchor", "middle")
.attr("font-family", "Verdana")
.attr("x", 0)
.attr("y", 0)
.attr("font-size", "10")
.text(function (d, i) {
return name;
});
// var group = g.append("g");
// group
// .append("rect")
// .attr("x", centroid[0] - w / 2)
// .attr("y", centroid[1] - h / 2)
// .attr("width", 80)
// .attr("height", 27)
// .attr("rx", 10)
// .attr("fill", "rgb(230, 230, 230)")
// .attr("stroke-width", "2")
// .attr("stroke", "#FFF");
// group
// .append("text")
// .attr("x", centroid[0] - w / 2)
// .attr("y", centroid[1] - h / 2)
// .text(function (d, i) {
// return name;
// });
});
// g.append("button")
// .attr("class", "wrap")
// .text((d) => d.properties.name);
}
);
function getCentroid(element) {
var bbox = element.getBBox();
return [bbox.x + bbox.width / 2, bbox.y + bbox.height / 2];
}
function clicked(d) {
var x, y, k;
if (d && centered !== d) {
// CENTROIDS for subregion provinces
var p = "";
json.features.forEach(function (f, i) {
if (f.properties.subregion === d.properties.subregion) {
p += path(f);
}
});
var tmp = svg.append("path");
tmp.attr("d", p);
var centroid = getCentroid(tmp.node());
tmp.remove();
// var centroid = path.centroid(p);
x = centroid[0];
y = centroid[1];
k = 2;
if (d.properties.subregion === "Northern Territories") {
k = 1.5;
}
centered = d;
} else {
x = width / 2;
y = height / 2;
k = 1;
centered = null;
}
g.selectAll("path").classed(
"active",
centered &&
function (d) {
return (
d.properties &&
d.properties.subregion === centered.properties.subregion
);
// return d === centered;
}
);
g.transition()
.duration(650)
.attr(
"transform",
"translate(" +
width / 2 +
"," +
height / 2 +
")scale(" +
k +
")translate(" +
-x +
"," +
-y +
")"
)
.style("stroke-width", 1.5 / k + "px");
}
.background-svg-map {
fill: none;
pointer-events: all;
}
#provinces {
fill: rgb(230, 230, 230);
}
#provinces > path:hover {
fill: #0630a6;
}
#provinces .active {
fill: #0630a6;
}
#province-borders-path {
fill: none;
stroke: #fff;
stroke-width: 1.5px;
stroke-linejoin: round;
stroke-linecap: round;
pointer-events: none;
}
<script src="https://d3js.org/d3.v3.min.js"></script>
<script src="https://d3js.org/topojson.v1.min.js"></script>
<div id="map-selector-app"></div>
The basic workflow for labels with a background is:
Position a parent g
Add the text
Add the rectangle and set it's size based on the bounding box of the parent g and then move it behind the text.
For d3v3, I'm going to actually add the rectangle before the text, but not style it until the text is added and the required size is known. If we append it after it will be infront of the text. In d3v4 there's a handy .lower() method that would move it backwards for us, in d3v3, there are ways to do this, but for simplicity, to ensure the rectangle is behind the text, I'll add it first
I'm going to deviate from your code at a more foundational level in my example. I'm not going to append child SVGs as this is introducing some sizing issues for you. Also, instead of using a loop to append the labels, I'm going to use a selectAll()/enter() cycle. This means I need a data array not an object ultimately. In order to help build that array I'll use an object though - by going through your json once, we can build a list of regions and create a geojson feature for each. The geojson feature is nice as it allows us to use path.centroid() which allows us to find the centroid of a feature without additional code.
So first, I need to create the data array:
var subregions = {};
json.features.forEach(function(feature) {
var subregion = feature.properties.subregion;
// Have we already encountered this subregion? If not, add it.
if(!(subregion in subregions)) {
subregions[subregion] = {"type":"FeatureCollection", features: [] };
}
// For every feature, add it to the subregion featureCollection:
subregions[subregion].features.push(feature);
})
// Convert to an array:
subregions = Object.keys(subregions).map(function(key) {
return { name: key, geojson: subregions[key] };
})
Now we can append the parent g with a standard d3 selectAll/enter statement:
// Create a parent g for each label:
var subregionsParent = g.selectAll(null)
.data(subregions)
.enter()
.append("g")
.attr("transform", function(d) {
// position the parent, so we don't need to position each child based on geographic location:
return "translate("+path.centroid(d.geojson)+")";
})
Now we can add the text and rectangle:
// add a rectangle to each parent `g`
var boxes = subregionsParent.append("rect");
// add text to each parent `g`
subregionsParent.append("text")
.text(function(d) { return d.name; })
.attr("text-anchor","middle");
// style the boxes based on the parent `g`'s bbox
boxes
.each(function() {
var bbox = this.parentNode.getBBox();
d3.select(this)
.attr("width", bbox.width + 10)
.attr("height", bbox.height +10)
.attr("x", bbox.x - 5)
.attr("y", bbox.y - 5)
.attr("rx", 10)
.attr("ry", 10)
.attr("fill","#ccc")
})
You can see the centroid method (whether using your existing function or path.centroid()) can be a little dumb when it comes to placement given some of the overlap on the map. There are ways you could modify that - perhaps adding offsets to the data, or manual exceptions when adding the text. Though on a larger SVG there shouldn't be overlap. Annotations are notoriously difficult to do.
Here's my result with the above:
And a snippet to demonstrate (I've removed a fair amount of unnecessary code to make a simpler example, but it should retain the functionality of your example):
var width = 500,
height = 275,
centered;
var projection = d3.geo.conicConformal()
.rotate([103, 0])
.center([0, 63])
.parallels([49, 77])
.scale(500)
.translate([width / 2.5, height / 2])
var path = d3.geo.path().projection(projection);
var svg = d3
.select("#map-selector-app")
.append("svg")
.attr("viewBox", `0 0 ${width} ${height}`);
var g = svg.append("g");
d3.json("https://gist.githubusercontent.com/KatFishSnake/7f3dc88b0a2fa0e8c806111f983dfa60/raw/7fff9e40932feb6c0181b8f3f983edbdc80bf748/canadaprovtopo.json",
function (error, canada) {
if (error) throw error;
json = topojson.feature(canada, canada.objects.canadaprov);
var provinces = g.append("g")
.attr("id", "provinces")
.selectAll("path")
.data(json.features)
.enter()
.append("path")
.attr("d", path)
.on("click", clicked);
g.append("g")
.attr("id", "province-borders")
.append("path")
.datum(topojson.mesh(canada, canada.objects.canadaprov, function (a,b) { return a !== b; }))
.attr("id", "province-borders-path")
.attr("d", path);
// Add labels:
// Get the data:
var subregions = {};
json.features.forEach(function(feature) {
var subregion = feature.properties.subregion;
if(!(subregion in subregions)) {
subregions[subregion] = {"type":"FeatureCollection", features: [] };
}
subregions[subregion].features.push(feature);
})
// Convert to an array:
subregions = Object.keys(subregions).map(function(key) {
return { name: key, geojson: subregions[key] };
})
// Create a parent g for each label:
var subregionsParent = g.selectAll(null)
.data(subregions)
.enter()
.append("g")
.attr("transform", function(d) {
return "translate("+path.centroid(d.geojson)+")";
})
var boxes = subregionsParent.append("rect");
subregionsParent.append("text")
.text(function(d) { return d.name; })
.attr("text-anchor","middle");
boxes
.each(function() {
var bbox = this.parentNode.getBBox();
d3.select(this)
.attr("width", bbox.width + 10)
.attr("height", bbox.height +10)
.attr("x", bbox.x - 5)
.attr("y", bbox.y - 5)
.attr("rx", 10)
.attr("ry", 10)
.attr("fill","#ccc")
})
// End labels.
function getCentroid(element) {
var bbox = element.getBBox();
return [bbox.x + bbox.width / 2, bbox.y + bbox.height / 2];
}
function clicked(d) {
var x, y, k;
if (d && centered !== d) {
// CENTROIDS for subregion provinces
var p = "";
json.features.forEach(function (f, i) {
if (f.properties.subregion === d.properties.subregion) {
p += path(f);
}
});
var tmp = svg.append("path");
tmp.attr("d", p);
var centroid = getCentroid(tmp.node());
tmp.remove();
x = centroid[0];
y = centroid[1];
k = 2;
if (d.properties.subregion === "Northern Territories") {
k = 1.5;
}
centered = d;
} else {
x = width / 2;
y = height / 2;
k = 1;
centered = null;
}
g.selectAll("path").classed(
"active",
centered &&
function (d) {
return (
d.properties &&
d.properties.subregion === centered.properties.subregion
);
}
);
g.transition()
.duration(650)
.attr("transform","translate(" + width / 2 + "," + height / 2 +")scale(" +
k +")translate(" + -x + "," + -y + ")"
)
.style("stroke-width", 1.5 / k + "px");
}
})
.background-svg-map {
fill: none;
pointer-events: all;
}
#provinces {
fill: rgb(230, 230, 230);
}
#provinces > path:hover {
fill: #0630a6;
}
#provinces .active {
fill: #0630a6;
}
#province-borders-path {
fill: none;
stroke: #fff;
stroke-width: 1.5px;
stroke-linejoin: round;
stroke-linecap: round;
pointer-events: none;
}
<script src="https://d3js.org/d3.v3.min.js"></script>
<script src="https://d3js.org/topojson.v1.min.js"></script>
<div id="map-selector-app"></div>
I'm trying to plot some cordinates on an image using d3 v4 following this Link.When i'm trying to pass my co-ordinates to the projection function it returns NAN for some of the data points. I got some help from here that javascript follows the following latitude and longitude convention but not sure how it exacty works.
This is the format of my data:
{coordinates: [60, 84],coordinates: [204, 92.4],coordinates: [117, 132.72]}
D3 code :
var el = d3.select('.js-map'),
// 150 DPI image
width = 300,
// 150 DPI image
height = 300;
var thisObj = this;
var projection = d3.geoMercator()
.scale(1)
.translate([0, 0])
console.log('projection', projection);
var path = d3.geoPath()
.projection(projection);
var map = el.append('svg')
.attr('width', width)
.attr('height', height);
map.append('image')
.attr('xlink:href', this.floorMaps[0])
.attr('width', width)
.attr('height', height);
this.floorSensorInfo.forEach((data, index) => {
var lonlat = projection(data.coordinates);
console.log('Longitude Latitude', lonlat);
I can see my data output like [2.0420352248333655, NaN]and not sure what happened exactly.
and moreover if someone can explain following the first link which i realy don't understand it would be really helpful
Exported bounds of raster image
rasterBounds = [[-122.7895, 45.4394], [-122.5015, 45.6039]]
Update:
#Andrew suggested to plot normal co-ordinates because latitude and longitude apply only to world maps. So i had pasted my below working code version now which is plotting the points on the image now.
var svg = d3.select("body")
.append("svg")
.attr("width",960)
.attr("height",500)
// image width and height in pixels, we don't want to skew this or scale this (then image units aren't straight pixels)
var imageWidth = 300;
var imageHeight = 168;
var color_hash = { 0 : ["apple", "green"],
1 : ["mango", "orange"],
2 : ["cherry", "red"]
}
function scale(coords) {
return [coords[0] * imageWidth / 100, coords[1] * imageHeight / 100];
}
svg.append("image")
.attr("width",imageWidth)
.attr("height",imageHeight)
.attr("x", 0) // could be non-zero, but we would have to shift each circle that many pixels.
.attr("y", 0)
.attr("xlink:href", this.floorMaps[0])
var data = this.floorSensorInfo
// var dataNest = d3.nest()
// .key(function (d) { return d['sensor_name']; })
// .entries(data)
data.forEach(function (d, i) {
svg.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cx", function(d) { return (d.value)[0]; })
.attr("cy", function(d) { return (d.value)[1]; })
.attr("r", 5)
.style("fill", function(d) {
var color = color_hash[data.indexOf(d)][1]
return color;
})
svg.append('text')
.attr("x", 20+(i)*100) // space legend
.attr("y", imageHeight+20)
// style the legend
.style("stroke", function () { // Add the colours dynamically
return d['color'] = color_hash[data.indexOf(d)][1];
})
//.attr("dy", ".35em")
.text( d.sensor_name);
//.text("jjjjjjj")
})}
var svg = d3.select("body")
.append("svg")
.attr("width",960)
.attr("height",500)
// image width and height in pixels, we don't want to skew this or scale this (then image units aren't straight pixels)
var imageWidth = 300;
var imageHeight = 168;
var color_hash = { 0 : ["apple", "green"],
1 : ["mango", "orange"],
2 : ["cherry", "red"]
}
function scale(coords) {
return [coords[0] * imageWidth / 100, coords[1] * imageHeight / 100];
}
svg.append("image")
.attr("width",imageWidth)
.attr("height",imageHeight)
.attr("x", 0) // could be non-zero, but we would have to shift each circle that many pixels.
.attr("y", 0)
.attr("xlink:href", this.floorMaps[0])
var data = this.floorSensorInfo
// var dataNest = d3.nest()
// .key(function (d) { return d['sensor_name']; })
// .entries(data)
data.forEach(function (d, i) {
svg.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cx", function(d) { return (d.value)[0]; })
.attr("cy", function(d) { return (d.value)[1]; })
.attr("r", 5)
.style("fill", function(d) {
var color = color_hash[data.indexOf(d)][1]
return color;
})
svg.append('text')
.attr("x", 20+(i)*100) // space legend
.attr("y", imageHeight+20)
// style the legend
.style("stroke", function () { // Add the colours dynamically
return d['color'] = color_hash[data.indexOf(d)][1];
})
//.attr("dy", ".35em")
.text( d.sensor_name);
//.text("jjjjjjj")
})}
javascript d3.js
I have an array of equally spaced values which I am using to draw concentric circles. I want to use an emanating effect, in essence, remove the outermost circle once its value exceeds the maximum value and add a new one at the center to compensate. I am unsure about manipulation on data set to remove and add new circle.
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
var svg = d3.select("body").append("svg");
var w = window.innerWidth;
var h = window.innerHeight;
var separation = Math.min(50, w/12);
var n=Math.floor((w/2)/separation);
var ellipse=new Array(n);
for(var i=1;i<=n;i++){
ellipse[i-1]=(i*separation);
}
svg.attr("width", w).attr("height", h);
var g = svg.append("g");
var e=g.selectAll("ellipse")
.data(ellipse)
.enter()
.append("ellipse")
.attr("cx", w/2)
.attr("cy", h/2)
.attr("rx",0)
.attr("ry",0)
.transition()
.duration(5000)
.attr("rx", function(d,i){return ellipse[i];})
.attr("ry", function(d,i){return ellipse[i]/5;});
loop();
function loop(){
e.transition()
.attr("rx", function(d,i){
return ellipse[i]+separation;
})
.attr("ry", function(d,i){
return (ellipse[i]+separation)/5;
})
.on("end",loop());
}
</script>
You could approach it with a remove().exit() and enter().append() selection for each ring - but essentially you always have the same number of rings on the screen. Why not just recycle the same elements? When the size hits a threshold, reset it to zero, or some other starting value?
Something along the lines of:
var scale = d3.scaleLinear()
.range(["orange","steelblue","purple"])
.domain([0,60]);
var data = [0,10,20,30,40,50,60];
var width = 200;
var height = 200;
var svg = d3.select("body")
.append("svg")
.attr("width",width)
.attr("height",height);
var circles = svg.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("r",function(d) { return d; })
.attr("transform","translate(80,80)")
.attr("fill","none")
.style("stroke-width","4")
.style("stroke",function(d) { return scale(d) });
function transition() {
// Update data, max d is 60:
data = data.map(function(d) { return d == 60 ? 0 : d + 10});
var i = 0;
// Grow circles
circles
.data(data)
.filter(function(d) { return d > 0 })
.transition()
.ease(d3.easeLinear)
.attr("r", function(d) { return d; })
.style("stroke", function(d) { return scale(d) })
.style("opacity",function(d) { return d == 60 ? 0 : 1 })
.duration(1000)
.on("end",function(){if(++i==circles.size()-1) { transition(); } });
// Reset circles where r == 0
circles
.filter(function(d) { return d == 0 })
.attr("r", 0)
.style("opacity",1)
.style("stroke",function(d) { return scale(d); });
}
transition();
<script src="https://d3js.org/d3.v4.min.js"></script>
Note that .on("end",... triggers on each element's transition end - this is why I count to see if all elements are done transitioning before running the transition function again.
[Update]
Click jsfiddle here.
[Original Post]
How can I limit the coordinates of cross hair when moving the mouse? Notice when I move my mouse to the left of x-axis or bottom of y-axis, the cross hair and text still shows. I want the cross hair and text to stop showing when either mouse is moved to the left of x-axis or bottom of y-axis.
I have tried to add if else to limit the cross hair, but it didn't work. For instance, I tried something like .style("display", (xCoord>=minX & xCoord<=maxX & yCoord>=minY & yCoord<=maxY) ? "block" : "none") in the addCrossHair() function.
<!DOCTYPE html>
<meta charset="utf-8">
<head>
<style>
.axis path,
.axis line
{
fill:none;
rendering:crispEdges;
stroke:black;
width:2.5;
}
</style>
</head>
<body>
<script src="d3.min.js"></script>
<script>
var width = 800,
height = 600;
var randomX = [],
randomY = [];
for (var i = 0; i <= 500; i++) {
randomX[i] = Math.random() * 400;
randomY[i] = Math.random() * 400;
}
var minX = d3.min(randomX),
maxX = d3.max(randomX),
minY = d3.min(randomY),
maxY = d3.max(randomY);
var xScale = d3.scale.linear().domain([minX, maxX]).range([0, width]);
var xAxis = d3.svg.axis().scale(xScale).orient("bottom");
var yScale = d3.scale.linear().domain([minY, maxY]).range([height, 0]);
var yAxis = d3.svg.axis().scale(yScale).orient("left");
var svgContainer = d3.select("body").append("div").append("svg").attr("width", width).attr("height", height);
var svg = svgContainer.append("g").attr("transform", "translate(50, 50)");
svg.append("g").attr("class", "axis").attr("transform", "translate(0,530)").call(xAxis);
svg.append("g").attr("class", "axis").call(yAxis);
var crossHair = svg.append("g").attr("class", "crosshair");
crossHair.append("line").attr("id", "h_crosshair") // horizontal cross hair
.attr("x1", 0)
.attr("y1", 0)
.attr("x2", 0)
.attr("y2", 0)
.style("stroke", "gray")
.style("stroke-width", "1px")
.style("stroke-dasharray", "5,5")
.style("display", "none");
crossHair.append("line").attr("id", "v_crosshair") // vertical cross hair
.attr("x1", 0)
.attr("y1", 0)
.attr("x2", 0)
.attr("y2", 0)
.style("stroke", "gray")
.style("stroke-width", "1px")
.style("stroke-dasharray", "5,5")
.style("display", "none");
crossHair.append("text").attr("id", "crosshair_text") // text label for cross hair
.style("font-size", "10px")
.style("stroke", "gray")
.style("stroke-width", "0.5px");
svgContainer.on("mousemove", function () {
var xCoord = d3.mouse(this)[0] - 50,
yCoord = d3.mouse(this)[1] - 50;
addCrossHair(xCoord, yCoord);
})
.on("mouseover", function () {d3.selectAll(".crosshair").style("display", "block");})
.on("mouseout", function () {d3.selectAll(".crosshair").style("display", "none");});
function addCrossHair(xCoord, yCoord) {
// Update horizontal cross hair
d3.select("#h_crosshair")
.attr("x1", xScale(minX))
.attr("y1", yCoord)
.attr("x2", xScale(maxX))
.attr("y2", yCoord)
.style("display", "block");
// Update vertical cross hair
d3.select("#v_crosshair")
.attr("x1", xCoord)
.attr("y1", yScale(minY))
.attr("x2", xCoord)
.attr("y2", yScale(maxY))
.style("display", "block");
// Update text label
d3.select("#crosshair_text")
.attr("transform", "translate(" + (xCoord + 5) + "," + (yCoord - 5) + ")")
.text("(" + xScale.invert(xCoord) + " , " + yScale.invert(yCoord) + ")");
}
svg.selectAll("scatter-dots")
.data(randomY)
.enter().append("svg:circle")
.attr("cy", function (d) {
return yScale(d);
})
.attr("cx", function (d, i) {
return xScale(randomX[i]);
})
.style("fill", "brown")
.attr("r", 3)
</script>
</body>
The way to do this is to make the actual canvas (i.e. where you're drawing the dots) a separate g element from the ones that the axes are rendered into. Then the canvas can be translated such that it sits to the right and above the axes. The crosshair handler would be attached to this canvas g element (which is not quite straightforward, see this question) and the crosshairs or dots won't appear outside of the canvas.
Complete demo here.
I spent hours trying to figure out why my code was not working. I then arbitrarily moved my button code from after the D3 code (at the end between </script> and </body>) to the top (between <script type="text/javascript"> and <body>). It works now, but I don't know why. I don't want to make this mistake again or confuse myself in the future.
<body>
<button>Update</button>
<script type="text/javascript">
var w = 500;
var h = 500;
var barPadding = 1;
var dataset = [ ];
for (var i = 0; i < 14; i++) {var newNumber = Math.round(Math.random() * 70);
dataset.push(newNumber);}
//Create SVG element
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
//Create Scales for Data conversion
var xScale = d3.scale.linear()
.domain([0, d3.max(dataset, function(d,i) {return d;})]) //input
.range([0,w]); // output
var yScale = d3.scale.ordinal()
.domain(d3.range(dataset.length))
.rangeRoundBands([0, h], 0.05); //Vertical separation + barpadding
svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("x", 3)
.attr("y", function (d,i) {return yScale(i);})
.attr("width", function(d,i) {return xScale(d);})
.attr("height", yScale.rangeBand())
.attr("fill", function(d) {return "rgb(" + (d * 10) + ", 0,0 )";});
svg.selectAll("text")
.data(dataset)
.enter()
.append("text")
.text(function(d) {return d;})
.attr("x", function(d) {return xScale(d) -15;})
.attr("y", function(d, i) {return yScale(i) +5 +yScale.rangeBand() / 2;})
.attr("fill", "white")
.attr("font-family", "sans-serif")
.attr("text-anchor", "middle");
//Create Data Update and transition
d3.select("button")
.on("click", function() {
//New values for dataset
dataset = [ ];
for (var i = 0; i < 14; i++) {var newNumber = Math.round(Math.random() * 70);
dataset.push(newNumber);}
//Update all rects, and color gradient
svg.selectAll("rect")
.data(dataset)
.transition()
.attr("x", 3)
.attr("width", function(d,i) {return xScale(d);})
.attr("fill", function(d) {return "rgb(" + (d * 10) + ", 0,0 )";});
//Update text label and position
svg.selectAll("text")
.data(dataset)
.text(function(d) {return d;})
.attr("x", function(d) {return xScale(d) -15;})
.attr("y", function(d, i) {return yScale(i) +5 + yScale.rangeBand() / 2;});
});
</script>
</body>
If you're saying that the code as shown in your question works, with the <button> element before the <script> element, it's because <script> elements are executed as the browser encounters them, top-to-bottom while parsing the page. Which means that any JavaScript that you use is only able to reference elements that are higher in the page source because the browser doesn't know about the later elements yet.
Unless you have code within functions that don't get called until after the DOM is complete, for example if you assign a DOM ready or onload event handler, or a delegated click handler or something.