SERG
SERG

Reputation: 3971

D3js group zoom

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

Upvotes: 0

Views: 84

Answers (1)

deristnochda
deristnochda

Reputation: 585

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 class="viz"></div>

Upvotes: 1

Related Questions