I have a Choropleth map where the tooltip is working for most of it, but the central states are now showing the tooltip...in face, they are not even running the mouseout callback function at all (tested with a console.log command).
At first I was using d3-tip, and that wasn't working, and it was the first time attempting it, so I thought I might be doing something wrong, so I opted to implement a standard div that toggles between display: none and display: block and when it still wasn't working, I threw in a console.log command to see if the callback function was running at all, and it's not. It's mostly an issue with Kansas, but some of the counties in the surrounding states are having problems too. and I know it's not an issue with the data set, because the example given, which pulls from the same data set is working fine.
Here is the css for the tooltip:
#tooltip{
display: none;
background-color: rgba(32,32,32,1);
position: absolute;
border-radius: 10px;
padding: 10px;
width: 200px;
height: 40px;
color: white
}
and the JS code:
$(function(){
//svg setup
const svgPadding = 60;
const svgWidth = 1000;
const svgHeight = 600;
var svg = d3.select('body')
.append('svg')
.attr('width', svgWidth)
.attr('height', svgHeight)
.attr('id', 'map');
function createChart(topData, eduData){
//scales
var colorScale = d3.scaleSequential(d3.interpolateBlues);
var unitScale = d3.scaleLinear()
.domain(d3.extent(eduData.map(e => e.bachelorsOrHigher)))
.range([0,1])
//map
var path = d3.geoPath();
svg.selectAll('.county')
.data(topojson.feature(topData, topData.objects.counties).features)
.enter()
.append('path')
.attr('class', 'county')
.attr('d', path)
.attr('data-fips', d=>d.id)
.attr('eduIndex', d => eduData.map(e => e.fips).indexOf(d.id))
.attr('data-education', function(){
var index = d3.select(this).attr('eduIndex');
if (index == -1)return 0;
return eduData[
d3.select(this).
attr('eduIndex')
]
.bachelorsOrHigher
})
.attr('fill', function(){
var value = d3.select(this).attr('data-education');
return colorScale(unitScale(value));
})
.attr('stroke', function(){
return d3.select(this).attr('fill');
})
.on('mouseover', function(d){
var index = d3.select(this).attr('eduIndex');
var education = d3.select(this).attr('data-education');
var county = index == -1 ? 'unknown' : eduData[index].area_name;
console.log(county)
var tooltip = d3.select('#tooltip')
.style('left', d3.event.pageX + 10 + 'px')
.style('top', d3.event.pageY + 10 + 'px')
.style('display', 'block')
.attr('data-education', education)
.html(`${county}: ${education}`)
})
.on('mouseout', ()=>d3.select('#tooltip').style('display', 'none'));
svg.append('path')
.datum(topojson.mesh(topData, topData.objects.states, (a,b)=>a.id!=b.id))
.attr('d', path)
.attr('fill', 'rgba(0,0,0,0)')
.attr('stroke', 'black')
.attr('stroke-width', 0.4)
//legend scale
const legendWidth = 0.5 * svgWidth;
const legendHeight = 30;
const numCells = 1000;
const cellWidth = legendWidth/numCells;
const legendUnitScale = d3.scaleLinear()
.domain([0, legendWidth])
.range([0,1]);
//legend
var legend = svg.append('svg')
.attr('id', 'legend')
.attr('width', legendWidth)
.attr('height', legendHeight)
.attr('x', 0.5 * svgWidth)
.attr('y', 0)
for (let i = 0; i < numCells; i++){
legend.append('rect')
.attr('x', i * cellWidth)
.attr('width', cellWidth)
.attr('height', legendHeight - 10)
.attr('fill', colorScale(legendUnitScale(i*cellWidth)))
}
}
//json requests
d3.json('https://raw.githubusercontent.com/no-stack-dub-sack/testable-projects-fcc/master/src/data/choropleth_map/counties.json')
.then(function(topData){
d3.json('https://raw.githubusercontent.com/no-stack-dub-sack/testable-projects-fcc/master/src/data/choropleth_map/for_user_education.json')
.then(function(eduData){
createChart(topData, eduData);
});
});
});
The issue is that you are applying a fill to the state mesh. Let's change the fill from rgba(0,0,0,0) to rgba(10,10,10,0.1):
It should be clear now why the mouse interaction doesn't work in certain areas: the mesh is filled over top of it. Regardless of the fact you can't see the mesh due to it having 0 opacity, it still intercepts the mouse events.
The mesh is meant only to represent the borders: it is a collection of geojson lineStrings (see here too). The mesh is not intended to be filled, it only should have a stroke.
If you change the mesh fill to none, or the pointer events of the mesh to none, then the map will work as expected.
Related
I am trying to simulate an arrow feature similar to draw.io where on mouse hover on an object, i would like to show the blue dots on the center of each side and arrows on all 4 sides. Any idea how to implement the same.
Appreciate your response.. Thank you.
I've made an example using a square. Hovering spawns crosses and arrows along each side. I recommend reading this documentation about paths in SVG to see how I drew these shapes.
const size = 200,
margin = 100,
offset = 10,
crossesPerSide = 4;
const arrowPath = 'M-5,0 h10 v30 h10 l-15,15 l-15,-15 h10 Z';
const crossPath = 'M-6,-5 l12,10 m0,-10 l-12,10';
const g = d3.select('svg')
.append('g')
.on('mouseover', () => {
g.selectAll('.arrow')
.data('nesw'.split(''))
.enter()
.append('path')
.classed('arrow', true)
.attr('d', arrowPath)
.attr('transform', (d, i) => `
translate(${margin} ${margin})
translate(${size / 2} ${size + offset})
rotate(${i * 90} 0 ${-offset - size / 2})
`)
g.selectAll('.cross')
.data(d3.range(crossesPerSide * 4))
.enter()
.append('path')
.classed('cross', true)
.attr('d', crossPath)
.attr('transform', (d, i) => {
const sideIdx = Math.floor(i / crossesPerSide);
const crossIdx = i % crossesPerSide;
const spacePerCross = (size / crossesPerSide);
return `
translate(${margin} ${margin})
rotate(${sideIdx * 90} ${size / 2} ${size / 2})
translate(${spacePerCross * (crossIdx + 0.5)}, 0)
`;
})
})
.on('mouseleave', () => {
g.selectAll('.arrow').remove();
g.selectAll('.cross').remove();
});
g.append('rect')
.attr('x', margin)
.attr('y', margin)
.attr('width', size)
.attr('height', size)
.attr('stroke', 'black')
.attr('fill', 'white')
svg {
border: solid 1px red;
}
.cross {
stroke: darkblue;
stroke-width: 2;
opacity: 0.6;
}
.arrow {
fill: darkblue;
opacity: 0.2;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="400" height="400"></svg>
While relative position logic, selections and translating sound like separate issues at first glance, in my particular case, you will see how they are all interrelated.
Building off my dynamic button appending project, I now want to make a transition() effect where you can append things in any order, remove them in any order, and if something was removed from the top, the bottom elements will gracefully glide up to the top to fill the empty space. I have only got it semi functional, the issue is I can't find the right logic to select only the text that is below the current button event.
Skipping the logic, I did successfully use the transition as desired by simply using selectAll('text'). This only works if you have 2 items, once you append 3 things it gets messy, because it doesn't have the right logic to discern which to transition.
The other translating problem is pertaining to making the button move. The text transitions fine, but the button stubbornly stays put regardless of using .attr('top', new_value) or .attr('transform', 'translate(' + 0+','+new_value+')')
Go to the live example with comments and choose things from the drop down menu, click to remove the top one and you will see what I mean.
So to sum up:
How can I make the if () logic check for any text that is greater than the current top distance? (i.e. if there are text elements that are lower on the page than the current)
How can I tell D3 to select only the text that has a greater relative position (in the vertical sense, i.e. lower on the page)
How can I make D3 translate or update the button's top? Note in the example I'm using selectAll('button'), but ideally I'd need a more discerning selection.
You should use data binding to achieve this, see the render() function in my code.
var margins = {
top: 200,
right: 80,
bottom: 30,
left: 50
};
var width = 500;
var height = 200;
var itemHeight = 30;
var totalWidth = width + margins.left + margins.right;
var totalHeight = height + margins.top + margins.bottom;
var y = d3.scaleOrdinal();
var options = [
'Add Text',
'A',
'B',
'C',
'D',
'E',
'F',
'G',
'H',
'I',
];
d3.select('#choose')
.on('change', addText)
.selectAll('option')
.data(options)
.enter().append('option')
.attr('value', function(d) {
return d;
})
.text(function(d) {
return d;
});
var svg = d3.select('body')
.append('svg')
.attr('width', totalWidth)
.attr('height', totalHeight);
var graphGroup = svg.append('g')
.attr('transform', 'translate(' + margins.left + ',' + margins.top + ')');
svg.append('text')
.text('Mix and Match Text')
.attr('font-weight', 'bold')
.attr('x', 15)
.attr('y', 60)
.attr('font-size', '24px');
var count = 0;
var intCount = 0;
var textItems = [];
function addText() {
var chooseSel = document.getElementById('choose');
var choice = chooseSel.options[chooseSel.selectedIndex].value;
textItems.push(choice);
render();
}
function removeItem(item) {
textItems.splice(textItems.indexOf(item), 1);
render()
}
function render() {
y.range(textItems.map((item, i) => i * itemHeight + 10)).domain(textItems);
var items = graphGroup.selectAll('.item').data(textItems, d => d);
var enteringItems = items.enter()
.append('g')
.attr('class', 'item')
.attr('opacity', 0)
items.merge(enteringItems)
.transition()
.attr('opacity', 1)
.attr('transform', d => `translate(0, ${y(d)})`)
items.exit()
.transition()
.attr('opacity', 0)
.remove()
enteringItems
.append('text')
.text(d => d)
.style('text-align', 'center')
enteringItems
.append('text')
.attr('class', 'close-button')
.attr('transform', 'translate(20, 0)')
.text('x')
.on('click', d => removeItem(d))
}
text {
font-family: sans-serif, Play;
}
form .s1 {
position: absolute;
top: 180px;
left: 55px;
}
*:focus {
outline: none;
}
select {
background: #404040;
color: #ffffff;
}
select option:checked:after {
background: #ffffff;
color: #404040;
}
.item {
fill: black;
}
.close-button {
fill: darkgray;
cursor: pointer;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.8.0/d3.min.js"></script>
<form>
<select id='choose' class='s1'></select>
</form>
I'm pretty new to d3js and feeling a little overwhelmed here. I'm trying to figure out how to query a rotated rectangle's corner coordinates so i can place a circle on that location (eventually I'm going to use that as a starting coordinate for a line to link to other nodes).
Here is an image showing what I'm trying to do:
Currently I'm getting the circle on the left of the svg boundary below, I'm trying to place it roughly where the x is below.
Here is my code for the circle:
let rx = node.attr("x");
let ry = node.attr("y");
g.append("circle")
.attr("cx",rx)
.attr("cy",ry)
.attr("r",5);
Here is my jsFiddle: jsFiddle and a Stack Overflow snippet
let d3Root = 'd3-cpm';
let w = document.documentElement.clientWidth;
let h = document.documentElement.clientHeight;
//TODO put type any
let eData = {
width: 180,
height: 180,
padding: 80,
fill: '#E0E0E0',
stroke: '#c3c5c5',
strokeWidth: 3,
hoverFill: '#1958b5',
hoverStroke: '#0046ad',
hoverTextColor: '#fff',
rx: 18,
ry: 18,
rotate: 45,
label: 'Decision Node',
textFill: 'black',
textHoverFill: 'white'
};
let cWidth;
let cHeight = h;
d3.select(d3Root)
.append("div")
.attr("id", "d3-root")
.html(function () {
let _txt = "Hello From D3! <br/>Frame Width: ";
let _div = d3.select(this);
let _w = _div.style("width");
cWidth = parseInt(_div.style("width"));
_txt += cWidth + "<br/> ViewPort Width: " + w;
return _txt;
});
let svg = d3.select(d3Root)
.append("svg")
.attr("width", cWidth)
.attr("height", cHeight)
.call(d3.zoom()
//.scaleExtent([1 / 2, 4])
.on("zoom", zoomed));
;
let g = svg.append("g")
.on("mouseover", function (d) {
d3.select(this)
.style("cursor", "pointer");
d3.select(this).select("rect")
.style("fill", eData.hoverFill)
.style("stroke", eData.hoverStroke);
d3.select(this).select("text")
.style("fill", eData.textHoverFill);
})
.on("mouseout", function (d) {
d3.select(this)
.style("cursor", "default");
d3.select(this).select("rect")
.style("fill", eData.fill)
.style("stroke", eData.stroke);
d3.select(this).select("text")
.style("fill", eData.textFill);
});
let node = g.append("rect")
.attr("width", eData.width)
.attr("height", eData.height)
.attr("fill", eData.fill)
.attr("stroke", eData.stroke)
.attr("stroke-width", eData.strokeWidth)
.attr("rx", eData.rx)
.attr("ry", eData.ry)
.attr("y", eData.padding)
.attr('transform', function () {
let _x = calcXLoc();
console.log(_x);
return "translate(" + _x + "," + "0) rotate(45)";
})
.on("click", ()=> {
console.log("rect clicked");
d3.event.stopPropagation();
//this.nodeClicked();
});
let nText = g.append('text')
.text(eData.label)
.style('fill', eData.textFill)
.attr('x', calcXLoc() - 50)
.attr('y', eData.width + 10)
.attr("text-anchor", "middle")
.on("click", ()=> {
console.log("text clicked");
d3.event.stopPropagation();
//this.nodeClicked();
});
let rx = node.attr("x");
let ry = node.attr("y");
g.append("circle")
.attr("cx",rx)
.attr("cy",ry)
.attr("r",5);
function calcXLoc() {
return (cWidth / 2 - eData.width / 2) + eData.width;
}
function zoomed() {
g.attr("transform", d3.event.transform);
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<d3-cpm></d3-cpm>
You're applying a transform to your rect to position and rotate it. It has no x attribute, so that comes back as undefined. This gets you slightly closer:
let rx = parseInt(node.attr("x"), 10) | 0;
let ry = parseInt(node.attr("y"), 10) | 0;
let height = parseInt(node.attr("height"), 10) | 0;
let transform = node.attr("transform");
g.append("circle")
.attr("cx",rx + height)
.attr("cy",ry + height)
.attr("transform", transform)
.attr("r",5);
But note that this is going to get kind of clunky and difficult to deal with - it'd be better if your data was modeled in such a way that the circular points were handled in there as well and could be somehow derived/transformed consistently....
Updated fiddle: https://jsfiddle.net/dcw48tk6/7/
Image:
I'm implementing a chart using d3 that has a sliding x axis. Demo
I noticed that the amount of ticks (i.e. the amount of axis labels) keeps growing, meaning that the labels that slide out of the chart are not removed from the DOM.
Why are the old labels stay in the DOM, and how could I fix that?
const timeWindow = 10000;
const transitionDuration = 3000;
const xScaleDomain = (now = new Date()) =>
[now - timeWindow, now];
const totalWidth = 500;
const totalHeight = 200;
const margin = {
top: 30,
right: 50,
bottom: 30,
left: 50
};
const width = totalWidth - margin.left - margin.right;
const height = totalHeight - margin.top - margin.bottom;
const svg = d3.select('.chart')
.append('svg')
.attr('width', totalWidth)
.attr('height', totalHeight)
.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`)
svg
.append('rect')
.attr('width', width)
.attr('height', height);
// Add x axis
const xScale = d3.scaleTime()
.domain(xScaleDomain(new Date() - transitionDuration))
.range([0, width]);
const xAxis = d3.axisBottom(xScale);
const xAxisSelection = svg
.append('g')
.attr('transform', `translate(0, ${height})`)
.call(xAxis);
// Animate
const animate = () => {
console.log(d3.selectAll('.tick').size()); // DOM keeps growing!!!
xScale.domain(xScaleDomain());
xAxisSelection
.transition()
.duration(transitionDuration)
.ease(d3.easeLinear)
.call(xAxis)
.on('end', animate);
};
animate();
svg {
margin: 30px;
background-color: #ccc;
}
rect {
fill: #fff;
outline: 1px dashed #ddd;
}
<script src="https://unpkg.com/d3#4.4.1/build/d3.js"></script>
<div class="chart"></div>
Analysis
The axis component will actually try to remove the ticks, which are no longer visible. Examining the source code brings up the line:
tickExit.remove();
Debugging to this line shows, that the exit selection is correctly calculated, i.e. all exiting nodes are contained in tickExit. But the nodes will not be removed as expected, because you have an active transition running on them. The documentation has it:
# transition.remove() <>
For each selected element, removes the element when the transition ends, as long as the element has no other active or pending transitions. If the element has other active or pending transitions, does nothing.
Workaround
One—admittely hacky—workaround could make use of the way D3 fades the ticks, which are no longer visible. This is not very nice, though, because it relies on the inner workings of D3 and might break in the future, should this behavior be altered.
Because selection.remove() is not that faint hearted, it can be used to take care of the removal instead of using transition.remove(). Personally, I would use something along the following lines in your animate() function:
d3.selectAll(".tick")
.filter(function() {
return +d3.select(this).attr("opacity") === 1e-6;
})
.remove();
Because the axis component will eventually fade all non-visible ticks to an opacity of 1e-6 this can be used to discard those elements. Note, however, that the tick count will at first come up to some value other than the starting value, because the transition to the final opacity will take some time to complete. But, the excess tick count is small and can safely be ignored.
Have a look at the following working demo. In this example, the tick count will increase from the initial 10 to 19 and subsequently stay at this value.
const timeWindow = 10000;
const transitionDuration = 3000;
const xScaleDomain = (now = new Date()) =>
[now - timeWindow, now];
const totalWidth = 500;
const totalHeight = 200;
const margin = {
top: 30,
right: 50,
bottom: 30,
left: 50
};
const width = totalWidth - margin.left - margin.right;
const height = totalHeight - margin.top - margin.bottom;
const svg = d3.select('.chart')
.append('svg')
.attr('width', totalWidth)
.attr('height', totalHeight)
.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`)
svg
.append('rect')
.attr('width', width)
.attr('height', height);
// Add x axis
const xScale = d3.scaleTime()
.domain(xScaleDomain(new Date() - transitionDuration))
.range([0, width]);
const xAxis = d3.axisBottom(xScale);
const xAxisSelection = svg
.append('g')
.attr('transform', `translate(0, ${height})`)
.call(xAxis);
// Animate
const animate = () => {
console.log(d3.selectAll('.tick').size()); // DOM keeps growing!!!
d3.selectAll(".tick")
.filter(function() {
return +d3.select(this).attr("opacity") === 1e-6;
})
.remove();
xScale.domain(xScaleDomain());
xAxisSelection
.transition()
.duration(transitionDuration)
.ease(d3.easeLinear)
.call(xAxis)
.on('end', animate);
};
animate();
svg {
margin: 30px;
background-color: #ccc;
}
rect {
fill: #fff;
outline: 1px dashed #ddd;
}
<script src="https://d3js.org/d3.v4.js"></script>
<div class="chart"></div>
Anything below is my take on the comments to issue #23 "Axis labels are not removed from the DOM" opened by OP for the d3-axis module, which contains some really good points.
The comment by Mike Bostock provides a more in-depth look at the concurring transitions on the same element, which will eventually prevent the removal of the ticks:
The problem is that when the end event for the parent G element is dispatched, the axis has not yet removed the old ticks. The ticks are removed by transition.remove, which listens for the end event on the tick elements. The end event for the G element is dispatched prior to the end event for the tick elements, so you are starting a new transition that interrupts the old one before the axis has a chance to remove the old ticks.
The real gem whatsoever is to be found in the comment by #curran, who suggested to use setTimeout(animate). This is brilliant and, as far as I know, the only non-intrusive, non-hacky solution to this problem! By pushing the animate function to the end of the event loop, this will defer the creation of the next transition until after the actual transition has had the chance to clean up after itself.
And, to wrap up this theoretical discussion, the probably best conclusion to your actual problem seems to be Mike Bostock's:
If you want a real-time axis, you probably don’t want transitions. Instead, use d3.timer and redraw the axis with every tick.
I having problem of zoom over map. The actual problem is when i zoom map, the location showing on map using smiley could also zoom but i don't want to zoom smiley. It could stay at same size and place. Sometime smiley get overlap so to avoid this i am trying to solve the above problem but i don't have idea how to transform attribute contains many things like images and text on map of d3.js. Please have a look at jsfiddle link and you can see that at japan 3 smiley get overlap and keep overlapped even after zooming map.
My JSfiddle link
my code is following:
<!DOCTYPE html>
<meta charset="utf-8">
<style>
path {
stroke: white;
stroke-width: 0.25px;
fill: grey;
}
</style>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://d3js.org/topojson.v0.min.js"></script>
<script>
var width = 960,
height = 500;
var data = [
{
"code":"TYO",
"city":"TOKYO",
"country":"JAPAN",
"lat":"35.68",
"lon":"139.76"
},
{
"code":"OSK",
"city":"Osaka",
"country":"JAPAN",
"lat":" 34.40",
"lon":"135.37"
},
{
"code":"HISH",
"city":"Hiroshima",
"country":"JAPAN",
"lat":"34.3853",
"lon":"132.4553"
},
{
"code":"BKK",
"city":"BANGKOK",
"country":"THAILAND",
"lat":"13.75",
"lon":"100.48"
},
{
"code":"DEL",
"city":"DELHI",
"country":"INDIA",
"lat":"29.01",
"lon":"77.38"
},
{
"code":"SEA",
"city":"SEATTLE",
"country":"USA",
"lat":"38.680632",
"lon":"-96.5001"
}
];
var projection = d3.geo.mercator()
.center([0, 5 ])
.scale(200)
.rotate([-180,0]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var path = d3.geo.path()
.projection(projection);
var g = svg.append("g");
// load and display the World
d3.json("world-110m2.json", function(error, topology) {
// load and display the cities
function drawMap(data){
var circle = g.selectAll("circle")
.data(data)
.enter()
.append("g")
circle.append("circle")
.attr("cx", function(d) {
return projection([d.lon, d.lat])[0];
})
.attr("cy", function(d) {
return projection([d.lon, d.lat])[1];
})
.attr("r", 5)
.style("fill", "red");
circle.append("image")
.attr("xlink:href", "http://fc08.deviantart.net/fs71/f/2013/354/8/7/blinking_smiley__animated__by_mondspeer-d6ylwn3.gif")//http://t2.gstatic.//com/images?q=tbn:ANd9GcT6fN48PEP2-z-JbutdhqfypsYdciYTAZEziHpBJZLAfM6rxqYX";})
.attr("class", "node")
.attr("x", function(d) {
return (projection([d.lon, d.lat])[0]) - 8;
})
.attr("y", function(d) {
return (projection([d.lon, d.lat])[1])-8;
})
.attr("width",20)
.attr("height",20)
//});
}
g.selectAll("path")
.data(topojson.object(topology, topology.objects.countries)
.geometries)
.enter()
.append("path")
.attr("d", path)
drawMap(data);
});
// zoom and pan
var zoom = d3.behavior.zoom()
.on("zoom",function() {
g.attr("transform","translate("+
d3.event.translate.join(",")+")scale("+d3.event.scale+")");
g.selectAll("circle")
.attr("d", path.projection(projection));
g.selectAll("path")
.attr("d", path.projection(projection));
});
svg.call(zoom)
</script>
</body>
</html>
Any body help me to zoom only map image not smiley
Implement semantic zooming :)
Try use this example to change your code :) :
Semantic zoom on map with circle showing capital
JSFIDDLE : http://jsfiddle.net/xf7222dg/2/
The code below shrinks the 'circles' depending on scale
var zoom = d3.behavior.zoom()
.on("zoom",function() {
g.attr("transform","translate("+
d3.event.translate.join(",")+")scale("+d3.event.scale+")");
g.selectAll("circle")
.attr("r", function(){
var self = d3.select(this);
var r = 8 / d3.event.scale; // set radius according to scale
self.style("stroke-width", r < 4 ? (r < 2 ? 0.5 : 1) : 2); // scale stroke-width
return r;
});
});
Here is it working with your smileys: http://jsfiddle.net/dmn0d11f/7/
You have to change the 'width' of the nodes (images) not the radius like with the circles. So select the nodes and instead of changing 'r' change 'width' :
g.selectAll(".node")
.attr("width", function(){
var self = d3.select(this);
var r = 28 / d3.event.scale; // set radius according to scale
self.style("stroke-width", r < 4 ? (r < 2 ? 0.5 : 1) : 2); // scale stroke-width
return r;
});