I want the marker to stay the same size when the map zooms in (40px). As I understand when I scale a group the marker size scales too, so I need to update the marker size after group zoom. But when I try to use
g.selectAll(".mark")
.transition()
.duration(750)
.style("stroke-width", 1.5 / scale "px")
.attr("transform", "translate(" translate ")scale(" 1 / scale ")")
the marker flies away (changes it xy coordinates) Here is my code https://jsfiddle.net/6L4yu1kc/1/
Is it possible to not scale the marker size? Thanks
CodePudding user response:
Basically, this marker has to be treated exactly like any other svg object with dimensions specified by an absolute quantity. For the state borders you do
.attr("stroke-width", 1.5 / scale "px")
and for the marker you need to scale every attribute that is specified in terms of image.width
or image.height
, respectively. You can position the marker using
.attr('width', image.width)
.attr('height', image.height)
.attr("x", d => d[0] - image.width / 2)
.attr("y", d => d[1] - image.height)
Therefore, on zoom set these to
.attr('width', image.width / scale)
.attr('height', image.height / scale)
.attr("x", d => d[0] - image.width / scale / 2)
.attr("y", d => d[1] - image.height / scale)
In principle, you could also do this using a transform with scale 1/scale
but the translate then needs to take into account, that the x
and y
coordinates of the marker are scaled likewise. This would look something like that
.attr("transform", d => `translate(
${d[0] - image.width / scale / 2 - (d[0] - image.width / 2) / scale},
${d[1] - image.height / scale - (d[1] - image.height) / scale}
) scale(${1 / scale})`);
I would scale the coordinates directly instead of this obscure transform ;)
Note: Since it really looks like the marker is changing its size, I added an additional circle
outside the group
that does not move and scale. The change of size seems to be an optical illusion.
const markers = [{
address: 'TX',
lat: '29.613',
lng: '-98.293'
}]
const image = {
width: 40,
height: 40
}
var margin = {
top: 10,
bottom: 10,
left: 10,
right: 10
},
width = parseInt(d3.select('.viz').style('width')),
width = width - margin.left - margin.right,
mapRatio = 0.5,
height = width * mapRatio,
active = d3.select(null);
var svg = d3.select('.viz').append('svg')
.attr('class', 'center-container')
.attr('height', height margin.top margin.bottom)
.attr('width', width margin.left margin.right);
svg.append('rect')
.attr('class', 'background center-container')
.attr('height', height margin.top margin.bottom)
.attr('width', width margin.left margin.right)
.on('click', clicked);
d3.json('https://raw.githubusercontent.com/benderlidze/d3-usa-click/master/us.topojson')
.then(ready)
var projection = d3.geoAlbersUsa()
.translate([width / 2, height / 2])
.scale(width);
var path = d3.geoPath()
.projection(projection);
var g = svg.append("g")
.attr('class', 'center-container center-items us-state')
.attr('transform', 'translate(' margin.left ',' margin.top ')')
.attr('width', width margin.left margin.right)
.attr('height', height margin.top margin.bottom)
function ready(us) {
g.append("g")
.attr("id", "states")
.selectAll("path")
.data(topojson.feature(us, us.objects.states).features)
.enter().append("path")
.attr("d", path)
.attr("class", "state")
.on("click", clicked);
g.append("path")
.datum(topojson.mesh(us, us.objects.states, function(a, b) {
return a !== b;
}))
.attr("id", "state-borders")
.attr("d", path);
const markers_proj = markers.map(d => projection([d.lng, d.lat]));
g.selectAll("circle")
.data(markers_proj)
.enter()
.append("circle")
.attr("cx", d => d[0])
.attr("cy", d => d[1])
.attr("r", 5)
.style("fill", "white");
svg.selectAll("circle.test")
.data(markers_proj)
.enter()
.append("circle")
.attr("class", "test")
.attr("cx", d => d[0] - 10)
.attr("cy", d => d[1] 10)
.attr("r", 5)
.style("fill", "black");
g.selectAll(".mark")
.data(markers_proj)
.enter()
.append("image")
.attr('class', 'mark')
.attr("xlink:href", 'https://benderlidze.github.io/d3-usa-click/icon.png')
.attr('width', image.width)
.attr('height', image.height)
.attr("x", d => d[0] - image.width / 2)
.attr("y", d => d[1] - image.height);
}
function clicked(d) {
if (d3.select('.background').node() === this) return reset();
if (active.node() === this) return reset();
active.classed("active", false);
active = d3.select(this).classed("active", true);
var bounds = path.bounds(d),
dx = bounds[1][0] - bounds[0][0],
dy = bounds[1][1] - bounds[0][1],
x = (bounds[0][0] bounds[1][0]) / 2,
y = (bounds[0][1] bounds[1][1]) / 2,
scale = .9 / Math.max(dx / width, dy / height),
translate = [width / 2 - scale * x, height / 2 - scale * y];
const t = d3.transition().duration(750);
g.transition(t)
.attr("transform", `translate(${translate}) scale(${scale})`);
g.select("#state-borders")
.transition(t)
.style("stroke-width", `${1.5 / scale}px`);
g.selectAll("circle")
.transition(t)
.attr("r", 5 / scale);
g.selectAll(".mark")
.transition(t)
.attr('width', image.width / scale)
.attr('height', image.height / scale)
.attr("x", d => d[0] - image.width / scale / 2)
.attr("y", d => d[1] - image.height / scale);
}
function reset() {
active.classed("active", false);
active = d3.select(null);
const t = d3.transition().duration(750);
g.transition(t)
.attr("transform", `translate(${margin.left},${margin.top}) scale(1)`);
g.select("#state-borders")
.transition(t)
.style("stroke-width", "1px");
g.selectAll("circle")
.transition(t)
.attr("r", 5);
g.selectAll(".mark")
.transition(t)
.attr('width', image.width)
.attr('height', image.height)
.attr("x", d => d[0] - image.width / 2)
.attr("y", d => d[1] - image.height);
}
.background {
fill: none;
pointer-events: all;
}
#states {
fill: #3689ff;
}
#states .active {
fill: #0057ce;
}
#state-borders {
fill: none;
stroke: #fff;
stroke-width: 1.5px;
stroke-linejoin: round;
stroke-linecap: round;
pointer-events: none;
}
.state:hover {
fill: #0057ce;
}
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<script type="text/javascript" src="https://d3js.org/topojson.v3.min.js"></script>
<div ></div>