Related
As an exercise to learn D3, I used a dataset from a previous project on the locations and names of airports all over the world. I'm loading this into my webpage using D3.csv and plotting the points on a map using topojson.
At this point in my exercise, I'm trying to add a feature to let users zoom in & out on the world map. As you can imagine, there are a lot of airports and the map gets crowded since I haven't added any filter logic yet.
Darndest thing is, I can get the Zoom behavior to work on countries, but I'm unsure how to get it to work on the circles I've drawn. If I zoom in on my map using the scroll-wheel, the map zooms in, but the circles stay in place.
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script src="http://d3js.org/topojson.v1.min.js"></script>
<script src="http://labratrevenge.com/d3-tip/javascripts/d3.tip.v0.6.3.js"></script>
<style type="text/css">
.feature {
fill: none;
stroke: grey;
stroke-width: 1px;
stroke-linejoin: round;
}
.mesh {
fill: none;
stroke: lightgrey;
stroke-width: 2px;
stroke-linejoin: round;
}
h1 {
font-family: sans-serif;
}
svg {
background: #eee;
}
.sphere {
fill: #fff;
}
.land {
fill: #000;
}
.boundary {
fill: none;
stroke: #fff;
stroke-linejoin: round;
stroke-linecap: round;
vector-effect: non-scaling-stroke;
}
.overlay {
fill: none;
pointer-events: all;
}
circle{
fill: steelblue;
stroke-width: 1.5px;
}
.d3-tip {
line-height: 1;
font-weight: bold;
padding: 12px;
background: rgba(0, 0, 0, 0.8);
color: #fff;
border-radius: 2px;
}
/* Creates a small triangle extender for the tooltip */
.d3-tip:after {
box-sizing: border-box;
display: inline;
font-size: 10px;
width: 100%;
line-height: 1;
color: rgba(0, 0, 0, 0.8);
content: "\25BC";
position: absolute;
text-align: center;
}
/* Style northward tooltips differently */
.d3-tip.n:after {
margin: -1px 0 0 0;
top: 100%;
left: 0;
}
</style>
</head>
<body>
<h1>Lots of airports across the world</h1>
<script type="text/javascript">
var width = 950,
height = 550;
scale0 = (width - 1) / 2 / Math.PI;
var projection = d3.geo.mercator();
var zoom = d3.behavior.zoom()
.translate([width / 2, height / 2])
.scale(scale0)
.scaleExtent([scale0, 8 * scale0])
.on("zoom", zoomed);
var path = d3.geo.path()
.projection(projection);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g");
var g = svg.append("g");
var circle = svg.append("circle");
svg.append("rect")
.attr("class", "overlay")
.attr("width", width)
.attr("height", height);
svg
.call(zoom)
.call(zoom.event);
var tip = d3.tip()
.attr("class", "d3-tip")
.offset([-10, 0])
.html(function(d) {
return "Name" + ": " + d[2] + "<br>" + "Location" + ": " + d[3];
});
svg.call(tip);
d3.json("world-110m.v1.json", function(error, world) {
if (error) throw error;
g.append("g")
.attr("d", path)
.on("click", clicked)
.on("zoom", zoomed);
g.append("path")
.datum({type: "Sphere"})
.attr("class", "sphere")
.attr("d", path);
g.append("path")
.datum(topojson.merge(world, world.objects.countries.geometries))
.attr("class", "land")
.attr("d", path);
g.append("path")
.datum(topojson.mesh(world, world.objects.countries, function(a, b) { return a !== b; }))
.attr("class", "boundary")
.attr("d", path)
.on("click", clicked);
d3.csv("output.csv",
function(data) {return {name: data.Airport_name, location: data.Location_served,
long : +data.Longitude, lat : +data.Latitude}},
function(data) {
var new_array = data.map(function (d) {return [d.long, d.lat, d.name, d.location]});
console.log("new", new_array)
svg.selectAll("circle")
.data(new_array)
.enter()
.append("circle")
.attr("cx", function (d) { return projection(d)[0]; })
.attr("cy", function (d) { return projection(d)[1]; })
.attr("r", "2px")
.on("mouseover", tip.show)
.on("mouseout", tip.hide);
});
}) //closes the json, do not move.
// begin click-zoom listeners
function clicked(d) {
console.log("d:",d)
var centroid = path.centroid(d),
translate = projection.translate();
projection.translate([
translate[0] - centroid[0] + width / 2,
translate[1] - centroid[1] + height / 2
]);
zoom.translate(projection.translate());
g.selectAll("path").transition()
.duration(700)
.attr("d", path);
}
function zoomed() {
projection.translate(d3.event.translate).scale(d3.event.scale);
g.selectAll("path").attr("d", path);
}
</script>
</body>
So what starts looking like this
ends looking like this upon zooming in
I'd like the circles to move as well as the countries.
CSV sample:
Airport_name,DST,IATA,ICAO,Location_served,Time,Latitude,Longitude
Anaa Airport,,AAA,NTGA,"Anaa, Tuamotus, French Polynesia",UTC?10:00,-16.9419074,-144.8646172
Arrabury Airport,,AAB,YARY,"Arrabury, Queensland, Australia",UTC+10:00,-26.7606354,141.0269959
El Arish International Airport,,AAC,HEAR,"El Arish, Egypt",UTC+02:00,31.1272509,33.8045859
Adado Airport,,AAD,,"Adado (Cadaado), Galguduud, Somolia",UTC+03:00,9.56045635,31.65343724
Rabah Bitat Airport (Les Salines Airport),,AAE,DABB,"Annaba, Algeria",UTC+01:00,36.8970249,7.7460806
Apalachicola Regional Airport,Mar-Nov,AAF,KAAF,"Apalachicola, Florida, United States",UTC?05:00,29.7258675,-84.9832278
Arapoti Airport,Oct-Feb,AAG,SSYA,"Arapoti, Paraná, Brazil",UTC?03:00,-24.1458941,-49.8228117
Merzbrück Airport,Mar-Oct,AAH,EDKA,"Aachen, North Rhine-Westphalia, Germany",UTC+01:00,50.776351,6.083862
Arraias Airport,,AAI,SWRA,"Arraias, Tocantins, Brazil",UTC?03:00,-12.9287788,-46.9437231
Your zoom function does two things, it modifies the projection and updates the paths using the modified projection:
function zoomed() {
projection.translate(d3.event.translate).scale(d3.event.scale); // modify the projection
g.selectAll("path").attr("d", path); // update the paths
}
Ok, so in addition to modifying the paths on each zoom using the bound datum, we need to modify the circles:
function zoomed() {
projection.translate(d3.event.translate).scale(d3.event.scale); // modify the projection
g.selectAll("path").attr("d", path); // update the paths
// update the circles/points:
svg.selectAll("circle")
.attr("cx", function (d) { return projection(d)[0]; })
.attr("cy", function (d) { return projection(d)[1]; })
});
}
However this doesn't quite work, we need to see how you append the circles:
svg.selectAll("circle")
.data(new_array)
.enter()
.append("circle")
.attr("cx", function (d) { return projection(d)[0]; })
.attr("cy", function (d) { return projection(d)[1]; })
This is great if there is no circle already on the svg - but there is, you appended one here:
var circle = svg.append("circle");
Which means that the first airport in the array won't be added as there is already a circle in the svg for that item in the data array. A null selection (d3.selectAll(null)) will ensure that an item is entered for every item in the data array.
Most importantly here, is that the first circle doesn't have a bound datum until after the data has loaded. This will cause some issues when calling the zoom, there is no bound data to use to rescale the circle and you'll get an error. Instead, you could append the airports with a class and select these during zoom events.
In my example here I've used a null selection to enter the airports, and given them a class so I can easily select the circles that I want to re position based on an updated projection. (For demonstration, I also simplified the world map and increased the point radius).
This looks like:
function zoomed() {
projection.translate(d3.event.translate).scale(d3.event.scale);
g.selectAll("path").attr("d", path);
svg.selectAll(".airport")
.attr("cx", function (d) { return projection(d)[0]; })
.attr("cy", function (d) { return projection(d)[1]; })
}
With the enter being:
svg.selectAll() // selectAll() is equivilant to selectAll(null)
.data(new_array)
.enter()
.append("circle")
.attr("class","airport")
.attr("cx", function (d) { return projection(d)[0]; })
.attr("cy", function (d) { return projection(d)[1]; })
.attr("r", "6px")
.on("mouseover", tip.show)
.on("mouseout", tip.hide);
});
I would like to add directionality to my D3 graph. The json will have an attribute called "direction" and it will either be "--", "<-", or "->"
meaning:
source <- target (arrow from target to source)
source -> target (arrow from source to target)
source -- target (no direction)
I would like it to match the color of the links if possible (not crucial)
Any assistance would be welcome!
My script below
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.node {
stroke: #fff;
stroke-width: 1.5px;
}
.link {
stroke: #999;
stroke-opacity: .6;
}
.axis {
opacity: 0.5;
font: 10px sans-serif;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
.axis .domain {
fill: none;
stroke: #000;
stroke-opacity: .3;
stroke-width: 4px;
stroke-linecap: round;
}
.axis .halo {
fill: none;
stroke: #ddd;
stroke-width: 3px;
stroke-linecap: round;
}
text {
pointer-events: none;
font: 8px sans-serif;
stroke: none;
fill: black;
}
.slider .handle {
fill: #fff;
stroke: #000;
stroke-opacity: .5;
stroke-width: 1.25px;
cursor: grab;
}
div.tooltip {
position: absolute;
text-align: center;
width: 60px;
height: 28px;
padding: 2px;
font: 12px sans-serif;
background: lightsteelblue;
border: 0px;
border-radius: 8px;
pointer-events: none;
}
</style>
<body>
<script src="https://d3js.org/d3.v3.min.js"></script>
<script>
var width = 960,
height = 500;
var color = d3.scale.category20();
var force = d3.layout.force()
.charge(-120)
.linkDistance(30)
.size([width, height]);
var x = d3.scale.linear()
.domain([0, 20])
.range([250, 80])
.clamp(true);
var brush = d3.svg.brush()
.y(x)
.extent([0, 0]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var links_g = svg.append("g");
var nodes_g = svg.append("g");
// Define the div for the tooltip
var div = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(" + (width - 20) + ",0)")
.call(d3.svg.axis()
.scale(x)
.orient("left")
.tickFormat(function(d) {
return d;
})
.tickSize(0)
.tickPadding(12))
.select(".domain")
.select(function() {
return this.parentNode.appendChild(this.cloneNode(true));
})
.attr("class", "halo");
var slider = svg.append("g")
.attr("class", "slider")
.call(brush);
slider.selectAll(".extent,.resize")
.remove();
var handle = slider.append("circle")
.attr("class", "handle")
.attr("transform", "translate(" + (width - 20) + ",0)")
.attr("r", 5);
svg.append("text")
.attr("x", width - 15)
.attr("y", 60)
.attr("text-anchor", "end")
.attr("font-size", "12px")
.style("opacity", 0.5)
.text("degree threshold")
d3.json("test.json", function(error, graph) {
if (error) throw error;
graph.links.forEach(function(d, i) {d.i = i;});
graph.nodes.forEach(function(d, i) {d.i = i;});
function brushed() {
var value = brush.extent()[0];
if (d3.event.sourceEvent) {
value = x.invert(d3.mouse(this)[1]);
brush.extent([value, value]);
}
handle.attr("cy", x(value));
var threshold = value;
var thresholded_links = graph.links.filter(function(d) {
return (d.max_degree > threshold);
});
force
.links(thresholded_links);
var link = links_g.selectAll(".link")
.data(thresholded_links, function(d) {
return d.i;
});
link.enter().append("line")
.attr("class", "link")
.style("stroke-width", function(d) {
return Math.sqrt(d.value);
});
link.exit().remove();
force.on("tick", function() {
link.attr("x1", function(d) {return d.source.x;})
.attr("y1", function(d) {return d.source.y;})
.attr("x2", function(d) {return d.target.x;})
.attr("y2", function(d) {return d.target.y;});
node.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
});
force.start();
//console.log(link);
link.each(function(d) {
d3.select(this).style("stroke", d.min_degree >= value ? "#3182bd" : "#ccc")
});
node.each(function(d) {
d3.select(this).select("circle").style("fill", d.degree > value ? color(1) : d.weight > 0 ? color(2) : "#ccc")
});
node.each(function(d) {
d3.select(this).select("text").style("fill", d.degree > value ? color(1) : d.weight > 0 ? color(2) : "#ccc")
});
}
force
.nodes(graph.nodes);
var node = nodes_g.selectAll(".node")
.data(graph.nodes)
.enter()
.append('g')
.attr("class", "node")
.call(force.drag);
node.append("circle")
.attr("r", 5)
.style("fill", function(d) {
return color(1);
})
.on("mouseover", function(d) {
div.transition()
.duration(200)
.style("opacity", .9);
div.html(d.name + "\ndegree:" + d.degree)
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
})
.on("mouseout", function(d) {
div.transition()
.duration(500)
.style("opacity", 0);
});
node.append("text")
.attr("dx", 4)
.attr("dy", ".35em")
.text(function(d) {
return d.name;
});
brush.on("brush", brushed);
slider
.call(brush.extent([16.5, 16.5]))
.call(brush.event);
});
</script>
Here is a sample json: test.json
I implemented something like what I think you're trying to achieve in a geojson sankey. Maybe you can use some of the code. Here's a link to the block.
And part of the relevant code:
var arrow = L.polylineDecorator(line, { patterns: [{
offset: '55%',
symbol: L.Symbol.arrowHead({
polygon: true,
pathOptions: {
weight: lineWeight,
color: lineColor
}
})
}]});
Hope this helps.
Here is my gist, all the contents are in place. Why does it not draw the population and the tracts data?
var width = 960, height = 500;
var data; // declare a global variable
var svg = d3.select('body').append('svg')
.attr('width', width)
.attr('height', height)
var threshold = d3.scaleThreshold()
.domain([1, 10, 20, 200, 800, 2000, 5000, 10000])
.range(d3.schemeOrRd[9]);
d3.queue()
.defer(d3.json, 'https://umbcvis.github.io/classes/class-12/tracts.json')
.defer(d3.json, 'https://umbcvis.github.io/classes/class-12/population.json')
.await(ready);
// Note: scale and translate will be determined by the data
var projection = d3.geoConicConformal()
.parallels([38 + 18 / 60, 39 + 27 / 60])
.rotate([77, -37 - 40 / 60]);
var path = d3.geoPath()
.projection(projection);
function ready(error, json, population) {
if (error) throw error;
// Convert topojson to GeoJSON
geojson = topojson.feature(json, json.objects.tracts);
tracts = geojson.features;
// Set the projection's scale and translate based on the GeoJSON
projection.fitSize([960, 500], geojson);
// Extract an array of features (one tract for each feature)
tracts.forEach(function(tract) {
var countyfips = tract.properties.COUNTYFP;
var tractce = tract.properties.TRACTCE;
pop = population.filter(function(d) {
return (d[2] === countyfips) &&
(d[3] === tractce);
});
pop = +pop[0][0];
var aland = tract.properties.ALAND / 2589975.2356;
//area in square miles
tract.properties.density = pop / aland;
});
svg.selectAll('path.tract')
.data(tracts)
.enter()
.append('path')
.attr('class', 'tract')
// .attr('d', path)
.style('fill', function(d) {
return threshold(d.properties.density);
})
.style('stroke', '#000')
// Draw all counties in MD
fips = geojson.features.map(function(d) {
return d.properties.COUNTYFP;
});
uniqueFips = d3.set(fips).values();
counties = uniqueFips.map(function(fips) {
return json.objects.tracts.geometries
.filter(function(d) {
return d.properties.COUNTYFP === fips;
});
});
counties = counties.map(function(county) {
return topojson.merge(json, county);
})
svg.selectAll("path.county")
.data(counties)
.enter()
.append('path')
.attr('class', 'county')
.attr('d', path)
.style('stroke', 'red')
.style('stroke-width', '2px')
.style('fill', 'none');
// 1. NEW: Define an array with Rockville MD longitude & latitude
var Rockville = [-77.1528, 39.0840];
// 2. NEW: Add an HTML <div> element that will function as the tooltip
var tooltip = d3.select('body').append('div')
.attr('class', 'tooltip')
.text('Hello, world!')
// 3. NEW: Add a draggable circle to the map
// See: https://bl.ocks.org/mbostock/22994cc97fefaeede0d861e6815a847e)
var layer2 = svg.append("g");
layer2.append("circle")
.attr("class", "Rockville")
.attr("cx", projection(Rockville)[0])
.attr("cy", projection(Rockville)[1])
.attr("r", 10)
.style("fill", "yellow") // Make the dot yellow
.call(d3.drag() // Add the drag behavior
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
//Add legend
addLegend();
}
function addLegend() {
var formatNumber = d3.format("d");
var x = d3.scalePow().exponent('.15')
.domain([1, 80000])
.range([0, 300]);
var xAxis = d3.axisBottom(x)
.tickSize(13)
.tickValues(threshold.domain())
.tickFormat(formatNumber)
var g = svg.append("g")
.attr('transform', 'translate(100, 200)')
.call(xAxis);
g.select(".domain")
.remove();
g.selectAll("rect")
.data(threshold.range().map(function(color) {
var d = threshold.invertExtent(color);
if (d[0] == null) d[0] = x.domain()[0];
if (d[1] == null) d[1] = x.domain()[1];
return d;
}))
.enter().insert("rect", ".tick")
.attr("height", 8)
.attr("x", function(d) {
return x(d[0]);
})
.attr("width", function(d) {
return x(d[1]) - x(d[0]);
})
.attr("fill", function(d) {
return threshold(d[0]);
});
g.append("text")
.attr("fill", "#000")
.attr("font-weight", "bold")
.attr("text-anchor", "start")
.attr("y", -6)
.text("Population per square mile");
}
function dragstarted(d) {
d3.select(this).raise().classed("active", true);
}
function dragged(d) {
d3.select(this).attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
}
function dragended(d) {
d3.select(this).classed("active", false);
}
path {
fill: #555555;
stroke: #aaaaaa;
}
body {
position: absolute;
margin: 0px;
font: 16px sans-serif;
}
.info {
color: #000;
position: absolute;
top: 450px;
left: 800px;
}
.tooltip {
position: absolute;
visibility: visible;
background-color: #aaa;
padding: 5px;
}
/* This style is used when dragging the dot */
.active {
stroke: #000;
stroke-width: 2px;
}
path {
fill: #555555;
stroke: #aaaaaa;
}
svg {
background-color: #4682b4;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/topojson.v2.min.js"></script>
<script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script>
Gist: https://bl.ocks.org/MTClass/0fb9c567311cfd2ea31f884a974fd246
You have not defined a path for your tracts:
svg.selectAll('path.tract')
.data(tracts)
.enter()
.append('path')
.attr('class', 'tract')
// .attr('d', path)
.style('fill', function(d) {
return threshold(d.properties.density);
})
.style('stroke', '#000')
For some reason this is commented out:
.attr('d', path)
So your tracts have no shape.
example:
<path class="tract" style="fill: rgb(215, 48, 31); stroke: rgb(0, 0, 0);"></path>
Try uncommenting that line, and you should get:
var width = 960, height = 500;
var data; // declare a global variable
var svg = d3.select('body').append('svg')
.attr('width', width)
.attr('height', height)
var threshold = d3.scaleThreshold()
.domain([1, 10, 20, 200, 800, 2000, 5000, 10000])
.range(d3.schemeOrRd[9]);
d3.queue()
.defer(d3.json, 'https://umbcvis.github.io/classes/class-12/tracts.json')
.defer(d3.json, 'https://umbcvis.github.io/classes/class-12/population.json')
.await(ready);
// Note: scale and translate will be determined by the data
var projection = d3.geoConicConformal()
.parallels([38 + 18 / 60, 39 + 27 / 60])
.rotate([77, -37 - 40 / 60]);
var path = d3.geoPath()
.projection(projection);
function ready(error, json, population) {
if (error) throw error;
// Convert topojson to GeoJSON
geojson = topojson.feature(json, json.objects.tracts);
tracts = geojson.features;
// Set the projection's scale and translate based on the GeoJSON
projection.fitSize([960, 500], geojson);
// Extract an array of features (one tract for each feature)
tracts.forEach(function(tract) {
var countyfips = tract.properties.COUNTYFP;
var tractce = tract.properties.TRACTCE;
pop = population.filter(function(d) {
return (d[2] === countyfips) &&
(d[3] === tractce);
});
pop = +pop[0][0];
var aland = tract.properties.ALAND / 2589975.2356;
//area in square miles
tract.properties.density = pop / aland;
});
svg.selectAll('path.tract')
.data(tracts)
.enter()
.append('path')
.attr('class', 'tract')
.attr('d', path)
.style('fill', function(d) {
return threshold(d.properties.density);
})
.style('stroke', '#000')
// Draw all counties in MD
fips = geojson.features.map(function(d) {
return d.properties.COUNTYFP;
});
uniqueFips = d3.set(fips).values();
counties = uniqueFips.map(function(fips) {
return json.objects.tracts.geometries
.filter(function(d) {
return d.properties.COUNTYFP === fips;
});
});
counties = counties.map(function(county) {
return topojson.merge(json, county);
})
svg.selectAll("path.county")
.data(counties)
.enter()
.append('path')
.attr('class', 'county')
.attr('d', path)
.style('stroke', 'red')
.style('stroke-width', '2px')
.style('fill', 'none');
// 1. NEW: Define an array with Rockville MD longitude & latitude
var Rockville = [-77.1528, 39.0840];
// 2. NEW: Add an HTML <div> element that will function as the tooltip
var tooltip = d3.select('body').append('div')
.attr('class', 'tooltip')
.text('Hello, world!')
// 3. NEW: Add a draggable circle to the map
// See: https://bl.ocks.org/mbostock/22994cc97fefaeede0d861e6815a847e)
var layer2 = svg.append("g");
layer2.append("circle")
.attr("class", "Rockville")
.attr("cx", projection(Rockville)[0])
.attr("cy", projection(Rockville)[1])
.attr("r", 10)
.style("fill", "yellow") // Make the dot yellow
.call(d3.drag() // Add the drag behavior
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
//Add legend
addLegend();
}
function addLegend() {
var formatNumber = d3.format("d");
var x = d3.scalePow().exponent('.15')
.domain([1, 80000])
.range([0, 300]);
var xAxis = d3.axisBottom(x)
.tickSize(13)
.tickValues(threshold.domain())
.tickFormat(formatNumber)
var g = svg.append("g")
.attr('transform', 'translate(100, 200)')
.call(xAxis);
g.select(".domain")
.remove();
g.selectAll("rect")
.data(threshold.range().map(function(color) {
var d = threshold.invertExtent(color);
if (d[0] == null) d[0] = x.domain()[0];
if (d[1] == null) d[1] = x.domain()[1];
return d;
}))
.enter().insert("rect", ".tick")
.attr("height", 8)
.attr("x", function(d) {
return x(d[0]);
})
.attr("width", function(d) {
return x(d[1]) - x(d[0]);
})
.attr("fill", function(d) {
return threshold(d[0]);
});
g.append("text")
.attr("fill", "#000")
.attr("font-weight", "bold")
.attr("text-anchor", "start")
.attr("y", -6)
.text("Population per square mile");
}
function dragstarted(d) {
d3.select(this).raise().classed("active", true);
}
function dragged(d) {
d3.select(this).attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
}
function dragended(d) {
d3.select(this).classed("active", false);
}
path {
fill: #555555;
stroke: #aaaaaa;
}
body {
position: absolute;
margin: 0px;
font: 16px sans-serif;
}
.info {
color: #000;
position: absolute;
top: 450px;
left: 800px;
}
.tooltip {
position: absolute;
visibility: visible;
background-color: #aaa;
padding: 5px;
}
/* This style is used when dragging the dot */
.active {
stroke: #000;
stroke-width: 2px;
}
path {
fill: #555555;
stroke: #aaaaaa;
}
svg {
background-color: #4682b4;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/topojson.v2.min.js"></script>
<script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script>
I have a simple stacked bar chart :
The code is here.
I would like to have scroll-bar on the axis but as you can see in the link the scroll appears for the div container with the help of CSS.
But i need something like this chart with scroll!
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<link href='https://fonts.googleapis.com/css?family=Open+Sans:400,300,700' rel='stylesheet' type='text/css'>
<link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css">
<script src="https://ajax.goquery.min.js"></script>
<style>
.axis path,
.axis line {
fill: none;
stroke: #BDBDBD;
}
.axis text {
font-family: 'Open Sans regular', 'Open Sans';
font-size: 13px;
}
.y.axis{
direction: ltr;
}
.grid .tick {
stroke: lightgrey;
opacity: 0.7;
}
.grid path {
stroke-width: 0;
}
.rect {
stroke: lightgrey;
fill-opacity: 0.6;
}
.wrapperDiv {
Width: 984px;
height: 35px;
border: thin solid black;
#margin-top: 36px;
#margin-bottom: 48px;
#margin-right: 20px;
#margin-left: 0px;
}
.divChart {
float:left;
font-size:13px;
color : #424242;
font-family: 'Open Sans regular', 'Open Sans';
#border: thin solid white;
margin-top: -0px;
#margin-bottom: 48px;
#margin-right: 150px;
margin-left: 50px;
#background-color: lightgrey;
width: 984px;
height: 500px;
#padding: 25px;
border: thin solid navy;
#margin: 25px;
#max-height:500px;
overflow-y:scroll;
direction: rtl;
}
</style>
</head>
<body>
<div class="divChart" id="wrapper-chart">
<div id ="chartID" ></div>
</div>
<script src="http://d3js.org/d3.v3.min.js"></script><script>
<script>
var dataset = [{"key":"Completion","values":[{"name":"Module 1","value":0},{"name":"Module 2","value":0},{"name":"Module 3","value":0},{"name":"Module 4","value":0},{"name":"Module 5","value":0},{"name":"Module 6","value":0},{"name":"Module 7","value":0},{"name":"Module 8","value":0.56},{"name":"Module 9","value":13.24},{"name":"Module 10","value":12.66}]},{"key":"NonCompletion","values":[{"name":"Module 1","value":100},{"name":"Module 2","value":100},{"name":"Module 3","value":100},{"name":"Module 4","value":100},{"name":"Module 5","value":100},{"name":"Module 6","value":100},{"name":"Module 7","value":100},{"name":"Module 8","value":99.44},{"name":"Module 9","value":86.76},{"name":"Module 10","value":87.34}]}];
function intChart(chartID, dataset) {
var margins = {top: 20, right: 20, bottom: 30, left: 120};
var width = 880 - margins.left -margins.right;
var height = 5250- margins.top - margins.bottom;
var old_width = width,old_height= height;
var module_fixed = 80;
height = Math.floor((dataset[0].values.length * height)/module_fixed)
var x = d3.scale.ordinal().rangeRoundBands([0, width], .1,.1)
var y = d3.scale.linear().rangeRound([height, 0], .1);
var series = dataset.map(function(d) {
return d.key;
});
dataset = dataset.map(function(d) {
return d.values.map(function(o, i) {
// Structure it so that your numeric
// axis (the stacked amount) is y
return {
y: o.value,
x: o.name
};
});
});
var stack = d3.layout.stack();
stack(dataset);
var dataset = dataset.map(function(
group) {
return group.map(function(d) {
// Invert the x and y values, and y0 becomes x0
return {
x: d.y,
y: d.x,
x0: d.y0
};
});
});
var xMax = d3.max(dataset, function(
group) {
return d3.max(group, function(d) {
return d.x + d.x0;
});
});
var xScale = d3.scale.linear()
.domain([0, xMax])
.range([0, width]);
var moduleName = dataset[0]
.map(function(d) {
return d.y;
});
var yScale = d3.scale.ordinal()
.domain(moduleName)
.rangeRoundBands([height,0]);
var svg = d3.select('#chartID')
.append('svg')
.attr("width", width + margins.left +
margins.right)
.attr("height", height + margins.top +
margins.bottom)
.append('g')
.attr('transform', 'translate(60,' + margins.top +
')');
var xAxis = d3.svg.axis()
.scale(xScale)
.orient('bottom')
.ticks(2)
.tickSize(0)
.tickPadding(20)
.tickFormat(function(d) {
return d + "%";
});
var yAxis = d3.svg.axis()
.scale(yScale)
.orient('left')
.tickSize(0);
var colours = d3.scale.ordinal().range(
["#8bc34a", "#ff8a65"]);
var groups = svg.selectAll('g')
.data(dataset)
.enter()
.append('g').attr('class', 'stacked')
.style('fill', function(d, i) {
return colours(i);
});
var rects = groups.selectAll(
'stackedBar')
.data(function(d, i) {
return d;
})
.enter()
.append('rect')
.attr('class', 'stackedBar')
.attr('x', function(d) {
return xScale(d.x0);
})
.attr('y', function(d, i) {
return yScale(d.y);
})
.attr('height', 48)
.attr('width', 0)
rects.transition()
.delay(function(d, i) {
return i * 50;
})
.attr("x", function(d) {
return xScale(d.x0);
})
.attr("width", function(d) {
return xScale(d.x);
})
.duration(3000);
//Added
x.domain(dataset.map(function(d) {
return d.value;
}));
y.domain([0, d3.max(dataset, function(
d) {
return d.name;
})]);
svg.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0,' +height + ')')
.call(xAxis)
.append("text")
.attr("transform", "rotate(360)")
.attr("y",10)
.attr("x", 140)
.attr("dy", ".30em")
.text("Percentage of Students");
svg.append('g')
.attr('class', 'y axis')
.call(yAxis)
.selectAll("text")
.style("text-anchor", "end")
.attr("dx", "-.5em")
.attr("dy", ".15em")
.attr("y", "-")
.attr("opacity", 1)
.attr("transform", function(d) {
return "rotate(-40)"
})
// Draw Y-axis grid lines
svg.selectAll("line.y")
.data(y.ticks(2))
.enter().append("line")
.attr("class", "y")
.attr("x1", 0)
.attr("x2", 450)
.attr("y1", y)
.attr("y2", y)
.style("stroke", "#ccc");
}
$(document).ready(function(){
intChart("chartID", dataset);
});
</script>
Would appreciate any help.
Thanks in advance.
I don't think this can be done using D3 only. If you want to use CSS, you need to fix the position of the x-axis. You can add a separate DIV and SVG container for the X axis (these are not scrollable), and the rest of the chart in another.
I modified you code to do this see here. Please note that you code needs a lot of cleaning, as there are several non-functional parts that makes it really confusing.
The modifications are as follows:
HTML
Added a new DIV (xaxis)
<div id="wrapper-chart">
<div class="divChart" id="chartID"></div>
<div id="xaxis"></div>
</div>
CSS
Added styling for the new div (same as divChart but without the scrolling)
#xaxis {
float: left;
font-size: 13px;
color: #424242;
font-family: 'Open Sans regular', 'Open Sans';
width: 984px;
direction: rtl;
}
JS
A new SVG container for the x-axis. Notice the height attribute.
var xaxis_svg = d3.select('#xaxis')
.append('svg')
.attr("width", width + margins.left + margins.right)
.attr("height", margins.bottom)
.append('g')
.attr('transform', 'translate(60,0)');
Append the x-axis to the container.
xaxis_svg.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0,' + 0 + ')')
.call(xAxis)
.append("text")
.attr("y", 10)
.attr("x", 140)
.attr("dy", ".30em")
.text("Percentage of Students");
Hope this helps.
I'm trying to add a tooltip showing the name of districts when you hover over it in the map in d3.js. The input is a topojson file and I've been able to successfully generate the map with district boundaries and highlight the currently selected district.
For the tooltip I tried doing something similar to this, but nothing happens at all. The code I've used is given below. The tooltip code is towards the end.
var width = 960,
height = 600;
var projection = d3.geo.albers()
.center([87, 28])
.rotate([-85, 0])
.parallels([27, 32]);
var path = d3.geo.path()
.projection(projection);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
svg.append("rect")
.attr("width", width)
.attr("height", height);
var g = svg.append("g");
var div = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 1e-6);
d3.json("data/nepal3.json", function(error, npl) {
var districts = topojson.feature(npl, npl.objects.nepal_districts);
projection
.scale(1)
.translate([0, 0]);
var b = path.bounds(districts),
s = .95 / 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];
projection
.scale(s)
.translate(t);
g.selectAll(".nepal_districts")
.data(districts.features)
.enter().append("path")
.attr("class", function(d) { return "nepal_districts " + d.id; })
.attr("d", path)
.on("mouseover", function(d,i) {
d3.select(this.parentNode.appendChild(this)).transition().duration(300)
.style({'stroke-width':2,'stroke':'#333333','stroke-linejoin':'round','cursor':'pointer','fill':'#b9270b'});
})
.on("mouseout", function(d,i) {
d3.select(this.parentNode.appendChild(this)).transition().duration(100)
.style({'stroke-width':2,'stroke':'#FFFFFF','stroke-linejoin':'round','fill':'#3d71b6'});
});
g.append("path")
.datum(topojson.mesh(npl, npl.objects.nepal_districts, function(a, b) { return a !== b;}))
.attr("d", path)
.attr("class", "district-boundary");
/* Tooltip */
g.selectAll(".nepal_districts")
.data(districts.features)
.enter().append("text")
.append("svg:rect")
.attr("width", 140)
.attr("height", 140)
.text(function(d) { return d.properties.name; })
.on("mouseover", mouseover)
.on("mousemove", mousemove)
.on("mouseout", mouseout);
function mouseover() {
div.transition()
.duration(300)
.style("opacity", 1);
}
function mousemove() {
div
.text(d3.event.pageX + ", " + d3.event.pageY)
.style("left", (d3.event.pageX - 34) + "px")
.style("top", (d3.event.pageY - 12) + "px");
}
function mouseout() {
div.transition()
.duration(100)
.style("opacity", 1e-6);
}
});
The CSS is
div.tooltip {
position: absolute;
text-align: center;
width: 60px;
height: 28px;
padding: 2px;
font: 12px sans-serif;
background: #4c4c4c;
border: 0px;
border-radius: 8px;
pointer-events: none;
}
The code I added for "Tooltip" does nothing at all. What am I doing wrong here?
The topojson file has this format. I wanted to get the "name" property to show up in the Tooltip.
{
"type": "Topology",
"objects": {
"nepal_districts": {
"type": "GeometryCollection",
"geometries": [
{
"type": "Polygon",
"id": 0,
"properties": {
"name": "HUMLA"
},
"arcs": [
[
0,
1,
2,
3
]
]
},
Had a similar problem where I ended up adding absolute positioned tooltip to body element, and modyfying its placement according to mouse position.
Add to directive:
function addTooltip(accessor) {
return function(selection) {
var tooltipDiv;
var bodyNode = d3.select('body').node();
selection.on("mouseover", function(topoData, countryIndex) {
if (!accessor(topoData, countryIndex)) {
return;
}
// Clean up lost tooltips
d3.select('body').selectAll('div.tooltipmap').remove();
formatValue(topoData, countryIndex);
tooltipDiv = d3.select('body').append('div').attr('class', 'tooltipmap');
var absoluteMousePos = d3.mouse(bodyNode);
tooltipDiv.style('left', (absoluteMousePos[0] + 10) + 'px')
.style('top', (absoluteMousePos[1] - 15) + 'px')
.style('opacity', 1)
.style('z-index', 1070);
accessor(topoData, countryIndex) || '';
})
.on('mousemove', function(topoData, countryIndex) {
if (!accessor(topoData, countryIndex)) {
return;
}
var absoluteMousePos = d3.mouse(bodyNode);
tooltipDiv.style('left', (absoluteMousePos[0] + 10) + 'px')
.style('top', (absoluteMousePos[1] - 15) + 'px');
var tooltipText = accessor(topoData, countryIndex) || '';
tooltipDiv.html(tooltipText);
})
.on("mouseout", function(topoData, countryIndex) {
if (!accessor(topoData, countryIndex)) {
return;
}
tooltipDiv.remove();
});
};
.tooltipmap{
background-color: #000000;
margin: 10px;
height: 50px;
width: 150px;
padding-left: 10px;
padding-top: 10px;
border-radius: 5px;
overflow: hidden;
display: block;
color: #FFFFFF;
font-size: 12px;
position: absolute;
opacity: 1;
h6{
margin: 0;
padding: 0;
}
p{
color: #FFFFFF;
}
}
Hope it helps!