I've put together a choropleth map using d3, helped by examples written by Mike Bostock. I'm new to d3 (and HTML, JavaScript, CSS to be honest).
I've got as far as creating the map and the legend, and being able to switch between different data sets. The map and source code can be viewed here on bl.ocks.org
Glasgow Index of Deprivation 2012
The problem I'm having now is working out how to replace the map and legend content when switching between the different datasets. As you can see at the moment, when a different dataset is selected, the content is simply added on top of the existing content.
I've tried following the advice given by Stephen Spann in this answer, and the code he provided in a working fiddle. But to no avail.
As I understand, I should add the g append to the svg variable in the beginning like so...
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g");
Then select it when updating like so...
var appending = svg.selectAll("g")
.attr("class", "S12000046_geo")
.data(topojson.feature(glasgowdep, glasgowdep.objects.S12000046_geo).features);
// add new elements
appending.enter().append("path");
// update existing elements
appending.style("fill",
function (d) {
return color(choro[d.id]);
})
.style("stroke", "#cfcfcf")
.attr("d", path)
// rollover functionality to display tool tips
.on("mouseover", function (d) {
tip.show(d)
d3.select(this)
.transition().duration(200)
.style("fill", "red");
})
.on("mouseout", function () {
tip.hide()
d3.select(this)
.transition().duration(200)
.style("fill",
function (d) {
return color(choro[d.id]);
});
})
// build the map legend
var legend = d3.select('#legend')
.append('ul')
.attr('class', 'list-inline');
var keys = legend.selectAll('li.key')
.data(color.range());
var legend_items = ["Low", "", "", "", "", "", "", "", "High"];
keys.enter().append('li')
.attr('class', 'key')
.style('border-top-color', String)
.text(function (d, i) {
return legend_items[i];
});
// remove old elements
appending.exit().remove();
A solution could be the following: at your code in http://bl.ocks.org/niallmackenzie/8a763afd14e195154e63 try adding the following line just before you build the map legend (line 220 in index.html):
d3.select('#legend').selectAll('ul').remove();
Every time you update your data, you empty first the #legend.
Thanks to the advice from Lars and the solution proposed by nipro, the following works. By adding the following code just above the section that builds the legend, the legend is emptied first before it gets updated:
d3.select('#legend')
.selectAll('ul')
.remove();
// build the map legend
var legend = d3.select('#legend')
...
And by doing the same for the main map, we can first empty the map before updating it:
d3.select("g")
.selectAll("path")
.remove();
// build the choropleth map
var appending = svg.selectAll("g")
...
The full working updated code can been seen on bl.ocks.org here.
Related
I'm currently rendering a US map along with every district's border. The grabbing the features of the topojson, we have an array of ~13,000 rows.
I'm also joining data to the topojson file, and running through a csv of ~180,000 rows. I believe I've optimized this process of joining data by ID enough using memoization, where the CSV is essentially turned into a hash, and each ID is the key to it's row data.
This process^ is run 24 times in Next JS through SSG to further the user experience, and so all 24 versions of this map is calculated before the first visit of this deployment. I'm sadly timing out during deployment phase for this specific web page^.
I've inspected the program and seem to find that painting/filling each district may be what's causing the slowdown. Are there any tips you all use to optimize rendering an SVG map of many path elements? Currently the attributes to this map:
1: tooltip for each district styled in tailwind
2: around 5 properties turned to text from topojson file w/ joined data to display upon hover, displayed by tooltip
3: filled dynamically with this snippet which runs a function based on the district's property type
.attr('fill', function (d) {return figureOutColor(d['properties'].type);})
4: adding a mouseover, mousemove, and mouseout event handler to each district.
Edit: Code snippet of adding attrs to my map
export const drawMap = (svgRef: SVGSVGElement, allDistricts: any[]) => {
const svg = d3.select(svgRef);
svg.selectAll('*').remove();
const projection = d3.geoAlbersUsa().scale(900).translate([400, 255]);
const path = d3.geoPath().projection(projection);
const tooltip = d3
.select('body')
.append('div')
.attr(
'class',
'absolute z-10 invisible bg-white',
);
svg
.selectAll('.district')
.data(allDistricts)
.enter()
.append('path')
.attr('class', 'district stroke-current stroke-0.5')
.attr('transform', 'translate(0' + margin.left + ',' + margin.top + ')')
.attr('d', path)
.attr('fill', function (d) {
return figureOutColor(d['properties'].type);
})
.on('mouseover', function (d) {
return tooltip
.style('visibility', 'visible')
.text(`${d.properties.name});
})
.on('mousemove', function (data) {
return tooltip.style('top', d3.event.pageY - 40 + 'px').style('left', d3.event.pageX + 'px');
})
.on('mouseout', function (d) {
d3.select(this).classed('selected fill-current text-white', false);
return tooltip.style('visibility', 'hidden');
});
I am new to js & d3. I am trying to plot a graph with d3.js forced layout.
My purpose is to append labels ("XXX" & "OOO") on either sides of edges but it's not working.
Here the code snippet:
link.append('svg:text')
.attr('class', 'aEnd')
.text(function(d) { return "XXX"; });
link.append('svg:text')
.attr('class', 'zEnd')
.text(function(d) { return "OOO"; });
Here is the complete code I am working on.
JSFIDDLE : d3_graph_labelled_edge.js
Nesting a text element inside a line element is not allowed, as summed up here.
What you can do is wrapping the line elements inside g containers and append the link labels to them:
var glink = vis.selectAll("line.link")
.data(data.links)
.enter().append("svg:g");
var link = glink.append("svg:line")
[...]
glink.append('svg:text')
.attr('class', 'aEnd')
.text(function(d) { return "XXX"; });
Updated fiddle.
im am trying to put labels on the administrative regions on this map of Ukraine. Chrome developer tools tells me that they exist with the right label and coordinates. Nonetheless they dont appear on the map.
The map
The code ive used:
d3.json('ukraine.geojson', function(data){
var group = canvas.selectAll('g')
.data(data.features)
.enter()
.append('g')
var projection = d3.geo.mercator().scale([2400]).translate([-900,2650]);
var path = d3.geo.path().projection(projection);
var areas = group.append('path')
.attr('d',path)
.attr('class','area')
.attr('fill','steelblue');
group.append('text')
.attr('x', function(d){ return path.centroid(d)[0];})
.attr('y', function(d){ return path.centroid(d)[1];})
.attr('text',function(d){return d.properties.NAME_1;})
.style("font-size","14px");
Does anybody know, why they dont appear on the map ?
If you change your output from:
<text x="414.512507938402" y="272.3826318168442" text="Cherkasy" style="font-size: 14px;"></text>
to:
<text x="414.512507938402" y="272.3826318168442" style="font-size: 14px;">Cherkasy</text>
You will see the text appear.
If you look at your code you can drop the .attr('text',function(d){return d.properties.NAME_1;}) portion and use something like .text(function(d) {return d.properties.NAME_1;});.
I'm a total beginner with d3js, so please be patient if my question looks dumb.
I'm trying to reproduce a chord graph like the one proposed by Mike Bostock. In the code by Bostock if you go with your mouse on an arc, all the chords that are not involved (as target as well as source) in the arc will fade.
I'd like to change it in order to let all the chords fade except the one on which there is a mouse (in order to emphasize one single two-way relationship).
I've added a fade_single function that is triggered when the mouse is over a chord:
svg.append("g")
.attr("class", "chord")
.selectAll("path")
.data(chord.chords)
.enter().append("path")
.style("fill", function(d) { return fill(d.target.index); })
.attr("d", d3.svg.chord().radius(r0))
.style("opacity", 1)
.on("mouseover", fade_single(0.1))
.on("mouseout", fade_single(1));
The fade_single function follows:
function fade_single(opacity) {
return function(g, i) {
svg.selectAll("g.chord path")
.filter(function(d) {
//return d.source.index != 0 && d.target.index != 0;
})
.transition()
.style("opacity", opacity);
};
}
The problem is that I don't know what to put in the commented line, i.e. to filter out all the relationship that are have not the row and column of the single chord. I've tried to play with the subindexes but the parameter i only gives you the row, so I don't know how to isolate the chord I want to exclude from the fading.
Any idea? Any hint?
Thank you,
Elisa
To fade everything but the current elemeent, the easiest way is to use the this reference to the current DOM element:
function fade_single(opacity) {
return function() {
var me = this;
svg.selectAll("g.chord path")
.filter(function(d) {
return this != me;
})
.transition()
.style("opacity", opacity);
};
}
I have been using D3 to create fancy animated charts, and the examples are great. However, I'm trying to do something seemingly a lot more basic, and having trouble - binding data to a simple list of DIVs.
I set up enter() to initialize elements at opacity 0, transition() to fade them in, and exit() to fade them out and remove them. enter() and exit() seem to be working fine - however, when an update contains an existing element already in the list, it seems to get partially removed - the containing DIV remains, but the contents disappear. I can't understand why the contents of the element would get changed in this way.
My code is as follows:
var data = [...];
sorted = data.sort(function(a, b) { return d3.descending(a.id, b.id); });
var tweet = tweetsBox
.selectAll('div')
.data(sorted, function(d) { return d.id; });
var enterDiv = tweet.enter()
.append("div")
.attr("class", "tweetdiv")
.style("opacity", 0);
enterDiv.append("div")
.attr("class", "username")
.text(function(d) { return "#" + d.username });
enterDiv.append("div")
.attr("class", "displayname")
.text(function(d) { return d.displayname });
enterDiv.append("div")
.attr("class", "date")
.text(function(d) { return d.date });
enterDiv.append("div")
.attr("class", "text")
.text(function(d) { return d.text });
tweet.transition()
.delay(200)
.style("opacity", 1);
tweet.exit()
.transition()
.duration(200)
.style("opacity", 0)
.remove();
I also set up a jsFiddle here demonstrating the issue.
The problem is that you're selecting the divs you created, but create more than one div per data element. When updating, d3 tries to match the data to the nested divs. As you're already assigning a special class to the top-level divs, the fix is very simple. Replace
.selectAll('div')
with
.selectAll('.tweetdiv')