D3js 组缩放

D3js group zoom

我希望标记在地图放大 (40px) 时保持相同大小。据我了解,当我缩放组时,标记大小也会缩放,因此我需要在组缩放后更新标记大小。但是当我尝试使用

 g.selectAll(".mark")
                .transition()
                .duration(750)
                .style("stroke-width", 1.5 / scale + "px")
                .attr("transform", "translate(" + translate + ")scale(" + 1 / scale + ")")

标记飞走了(改变它的xy坐标) 这是我的代码 https://jsfiddle.net/6L4yu1kc/1/

是否可以不缩放标记大小?谢谢

基本上,必须像对待任何其他尺寸由绝对数量指定的 svg 对象一样对待此标记。对于州界,你做

.attr("stroke-width", 1.5 / scale + "px")

并且对于标记,您需要分别缩放根据 image.widthimage.height 指定的每个属性。您可以使用

定位标记
.attr('width', image.width)
.attr('height', image.height)
.attr("x", d => d[0] - image.width / 2)
.attr("y", d => d[1] - image.height)

因此,在缩放时将这些设置为

.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)

原则上,您也可以使用缩放比例为 1/scale 的变换来执行此操作,但需要考虑翻译,即标记的 xy 坐标同样缩放。这看起来像那样

.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})`);

我会直接缩放坐标而不是这种晦涩的变换 ;)

注意:由于看起来标记确实在改变其大小,因此我在 group 之外添加了一个额外的 circle,它不会移动和缩放。尺寸的变化似乎是一种错觉。

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>