D3 在调整大小窗口上更改 SVG 尺寸

Posted

技术标签:

【中文标题】D3 在调整大小窗口上更改 SVG 尺寸【英文标题】:D3 change SVG dimensions on resize window 【发布时间】:2021-04-09 16:45:58 【问题描述】:

我有一个项目,我正在使用 D3 js 创建一些图表。我试图让这些图表在窗口大小发生变化时做出响应。为此,我已经使用 viewbox 来定义 svg:

var svg = d3
      .select(this.$refs["chart"])
      .classed("svg-container", true)
      .append("svg")
      .attr("class", "chart")
      .attr(
        "viewBox",
        `0 0 $width + margin.left + margin.right $height +
          margin.top +
          margin.bottom`
      )
      .attr("preserveAspectRatio", "xMinYMin meet")
      .classed("svg-content-responsive", true)
      .append("g")
      .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

我还使用将宽度和高度设置为与 SVG 所在的 div 相同。所以这个图表使用与它里面的 div 相同的大小:

 width = this.$refs["chart"].clientWidth - margin.left - margin.right,
 height = this.$refs["chart"].clientHeight - margin.top - margin.bottom;

此 div 的宽度和高度设置为其父 div 的 100%。因此,当我调整窗口大小时,svg 所在的 div 可以更改大小和纵横比。 所以这就是页面加载时图表最初的样子。所以它从它所在的 div 中获取它的高度和宽度:

但是当我调整图表大小时,它仍然可以适应父 div 的新宽度。但高度也随之变化。所以我假设纵横比保持不变:

我尝试在窗口调整大小时更新 svg 视口。但是当我在 Chrome 中检查开发人员工具的 DOM 中的 SVG 元素时,viewuwport 没有更新。我添加了控制台日志以检查父级的宽度和高度是否也发生了变化,并且它们似乎发生了变化。但更新后的视口不会应用于 svg:

d3.select(window).on("resize", () => 
      svg.attr(
        "viewBox",
        `0 0 $this.$refs["chart"].clientWidth $this.$refs["chart"].clientHeight`
      );
    );

<!DOCTYPE html>
<html>

<head>
  <title></title>
  <script src="https://unpkg.com/vue"></script>
  <script src="https://d3js.org/d3.v6.js"></script>
  <style>
    .area 
      fill: url(#area-gradient);
      stroke-width: 0px;
    
    
    body
      width: 100%;
      height: 100%;
    
    
    .app
      width: 100%;
      height: 100%;
    
    
    #page
      width: 100%;
      height: 100%;
    
    
    .my_dataviz
      width: 100%;
      height: 100%;
    
  </style>
</head>

<body>
  <div id="app">
    <div class="page">
        <div id="my_dataviz" ref="chart"></div>
    </div>
  </div>

  <script>
    new Vue(
      el: '#app',
      data: 
        type: Array,
        required: true,
      ,
      mounted() 

        const minScale = 0,
          maxScale = 35;

        var data = [
            key: 'One',
            value: 33,
          ,
          
            key: 'Two',
            value: 30,
          ,
          
            key: 'Three',
            value: 37,
          ,
          
            key: 'Four',
            value: 28,
          ,
          
            key: 'Five',
            value: 25,
          ,
          
            key: 'Six',
            value: 15,
          ,
        ];

        console.log(this.$refs["chart"].clientHeight)

        // set the dimensions and margins of the graph
        var margin = 
            top: 20,
            right: 0,
            bottom: 30,
            left: 40
          ,
          width =
          this.$refs["chart"].clientWidth - margin.left - margin.right,
          height =
          this.$refs["chart"].clientHeight - margin.top - margin.bottom;

        // set the ranges
        var x = d3.scaleBand().range([0, width]).padding(0.3);
        var y = d3.scaleLinear().range([height, 0]);

        // append the svg object to the body of the page
        // append a 'group' element to 'svg'
        // moves the 'group' element to the top left margin
        var svg = d3
          .select(this.$refs['chart'])
          .classed('svg-container', true)
          .append('svg')
          .attr('class', 'chart')
          .attr(
            'viewBox',
            `0 0 $width + margin.left + margin.right $
                height + margin.top + margin.bottom
              `
          )
          .attr('preserveAspectRatio', 'xMinYMin meet')
          .classed('svg-content-responsive', true)
          .append('g')
          .attr(
            'transform',
            'translate(' + margin.left + ',' + margin.top + ')'
          );

        // format the data
        data.forEach(function(d) 
          d.value = +d.value;
        );

        // Scale the range of the data in the domains
        x.domain(
          data.map(function(d) 
            return d.key;
          )
        );
        y.domain([minScale, maxScale]);

        //Add horizontal lines
        let oneFourth = (maxScale - minScale) / 4;

        svg
          .append('svg:line')
          .attr('x1', 0)
          .attr('x2', width)
          .attr('y1', y(oneFourth))
          .attr('y2', y(oneFourth))
          .style('stroke', 'gray');

        svg
          .append('svg:line')
          .attr('x1', 0)
          .attr('x2', width)
          .attr('y1', y(oneFourth * 2))
          .attr('y2', y(oneFourth * 2))
          .style('stroke', 'gray');

        svg
          .append('svg:line')
          .attr('x1', 0)
          .attr('x2', width)
          .attr('y1', y(oneFourth * 3))
          .attr('y2', y(oneFourth * 3))
          .style('stroke', 'gray');

        //Defenining the tooltip div
        let tooltip = d3
          .select('body')
          .append('div')
          .attr('class', 'tooltip')
          .style('position', 'absolute')
          .style('top', 0)
          .style('left', 0)
          .style('opacity', 0);

        // append the rectangles for the bar chart
        svg
          .selectAll('.bar')
          .data(data)
          .enter()
          .append('rect')
          .attr('class', 'bar')
          .attr('x', function(d) 
            return x(d.key);
          )
          .attr('width', x.bandwidth())
          .attr('y', function(d) 
            return y(d.value);
          )
          .attr('height', function(d) 

            console.log(height, y(d.value))
            return height - y(d.value);
          )
          .attr('fill', '#206BF3')
          .attr('rx', 5)
          .attr('ry', 5)
          .on('mouseover', (e, i) => 
            d3.select(e.currentTarget).style('fill', 'white');
            tooltip.transition().duration(500).style('opacity', 0.9);
            tooltip
              .html(
                `<div><h1>$i.key $
                    this.year
                  </h1><p>$converter.addPointsToEveryThousand(
                    i.value
                  ) kWh</p></div>`
              )
              .style('left', e.pageX + 'px')
              .style('top', e.pageY - 28 + 'px');
          )
          .on('mouseout', (e) => 
            d3.select(e.currentTarget).style('fill', '#206BF3');
            tooltip.transition().duration(500).style('opacity', 0);
          );

        // Add the X Axis and styling it
        let xAxis = svg
          .append('g')
          .attr('transform', 'translate(0,' + height + ')')
          .call(d3.axisBottom(x));

        xAxis
          .select('.domain')
          .attr('stroke', 'gray')
          .attr('stroke-width', '3px');
        xAxis.selectAll('.tick text').attr('color', 'gray');
        xAxis.selectAll('.tick line').attr('stroke', 'gray');

        // add the y Axis and styling it also only show 0 and max tick
        let yAxis = svg.append('g').call(
          d3
          .axisLeft(y)
          .tickValues([this.minScale, this.maxScale])
          .tickFormat((d) => 
            if (d > 1000) 
              d = Math.round(d / 1000);
              d = d + 'K';
            
            return d;
          )
        );

        yAxis
          .select('.domain')
          .attr('stroke', 'gray')
          .attr('stroke-width', '3px');
        yAxis.selectAll('.tick text').attr('color', 'gray');
        yAxis.selectAll('.tick line').attr('stroke', 'gray');

        d3.select(window).on('resize', () => 
          svg.attr(
            'viewBox',
            `0 0 $this.$refs['chart'].clientWidth $this.$refs['chart'].clientHeight`
          );
        );
      ,
    );
  </script>
</body>

</html>

【问题讨论】:

我试图重新创建您的问题here。从上面的代码 sn-p 中添加一些缺少的变量,您的代码似乎工作得很好。 @Mark 感谢您让我的代码在浏览器编辑器中运行。我遇到的问题是,当父 div 由于调整窗口大小而改变大小时。然后图形会缩小,但纵横比不会改变。然后它不再占据父 div 的全部高度。它仍然适合父 div。但我想让图表动态更新高度和宽度以始终完全显示在父 div 中。 @Mark 我也认为问题可能是因为我使用了.attr("preserveAspectRatio", "xMinYMin meet") 【参考方案1】:

对于 SVG 的“响应性”有不同的方法,尤其是在 D3 中。使用 viewBox 是一种处理方式,监听调整大小事件并重绘 svg 是另一种方式。如果您要监听调整大小事件并重新渲染,您需要确保您使用的是 D3 general update pattern。

1.您在使用 viewBox 和 preserveAspectRatio 时看到的行为是预期的。

2。在您的示例中,Vue 和 D3 似乎在谁控制 DOM 方面存在冲突。

以下是使用不同方法动态调整大小的一些示例。在全尺寸窗口中运行它们并使用控制台注销视口尺寸。

Sara Soueidan 的文章Understanding SVG Coordinate Systems 真的很好。 Curran Kelleher 的示例 here 将通用更新模式用于更惯用的内容。

真的希望这对项目有所帮助并祝你好运!如果您发现这回答了您的问题,请将其标记为已接受的答案。 ?

强制 D3 在调整大小事件时重新计算矩形和轴的大小(“粘”到容器的大小):

const margin = top: 20, right: 20, bottom: 50, left: 20
const width = document.body.clientWidth
const height = document.body.clientHeight
  
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;

const minScale = 0,
      maxScale = 35;

const xScale = d3.scaleBand()
    .range([0, width])
    .padding(0.3);;

const yScale = d3.scaleLinear()
    .range([0, height]);

const xAxis = d3.axisBottom(xScale)

const yAxis = d3.axisLeft(yScale)

const svg = d3.select("#chart")
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom)
  .append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

const data = [
  
    key: 'One',
    value: 33,
  ,
  
    key: 'Two',
    value: 30,
  ,
  
    key: 'Three',
    value: 37,
  ,
  
    key: 'Four',
    value: 28,
  ,
  
    key: 'Five',
    value: 25,
  ,
  
    key: 'Six',
    value: 15,
  ,
];

// format the data
data.forEach((d) => 
  d.value = +d.value;
);

// Scale the range of the data in the domains
xScale.domain(data.map((d) => d.key));
yScale.domain([minScale, maxScale]);

svg.append("g")
    .attr("class", "y axis")
    .call(yAxis);

svg.append("g")
    .attr("class", "x axis")
    .call(xAxis)
    .attr("transform", "translate(0," + height + ")")
  .append("text")
    .attr("class", "label")
    .attr("transform", "translate(" + width / 2 + "," + margin.bottom / 1.5 + ")")
    .style("text-anchor", "middle")
    .text("X Axis");


svg.selectAll(".bar")
    .data(data)
  .enter().append("rect")
    .attr("class", "bar")
    .attr("width", xScale.bandwidth())
    .attr('x', (d) => xScale(d.key))
    .attr("y", d => yScale(d.value))
    .attr('height', function (d) 
      return height - yScale(d.value);
    )
    .attr('fill', '#206BF3')
    .attr('rx', 5)
    .attr('ry', 5);



// Define responsive behavior
function resize() 
  var width = parseInt(d3.select("#chart").style("width")) - margin.left - margin.right,
  height = parseInt(d3.select("#chart").style("height")) - margin.top - margin.bottom;

  // Update the range of the scale with new width/height
  xScale.rangeRound([0, width], 0.1);
  yScale.range([height, 0]);

  // Update the axis and text with the new scale
  svg.select(".x.axis")
    .call(xAxis)
    .attr("transform", "translate(0," + height + ")")
    .select(".label")
      .attr("transform", "translate(" + width / 2 + "," + margin.bottom / 1.5 + ")");

  svg.select(".y.axis")
    .call(yAxis);

  // Force D3 to recalculate and update the line
  svg.selectAll(".bar")
    .attr("width", xScale.bandwidth())
    .attr('x', (d) => xScale(d.key))
    .attr("y", d => yScale(d.value))
    .attr('height', (d) => height - yScale(d.value));
;

// Call the resize function whenever a resize event occurs
d3.select(window).on('resize', resize);

// Call the resize function
resize();
.bar 
  fill: #206BF3;


.bar:hover 
  fill: red;
  cursor: pointer;


.axis 
  font: 10px sans-serif;


.axis path,
.axis line 
  fill: none;
  stroke: #000;
  shape-rendering: crispEdges;


#chart 
  width: 100%;
  height: 100%;
  position: absolute;
<!DOCTYPE html>
<meta charset="utf-8">
<head>
  <link rel="stylesheet" type="text/css" href="./style.css" />
</head>
<body>
<svg id="chart"></svg>
<script src="https://d3js.org/d3.v6.js"></script>

<script src="./chart.js"></script>
</body>

使用一般更新模式(用过渡来说明变化):

let data = [
  letter: 'A', frequency: 20,
  letter: 'B', frequency: 60,
  letter: 'C', frequency: 30,
  letter: 'D', frequency: 20,
];

chart(data);

function chart(data) 

  var svg = d3.select("#chart"),
    margin = top: 55, bottom: 0, left: 85, right: 0,
    width  = parseInt(svg.style("width")) - margin.left - margin.right,
    height = parseInt(svg.style("height")) - margin.top - margin.bottom;

  // const barWidth = width / data.length
  
  const xScale = d3.scaleBand()
    .domain(data.map(d => d.letter))
    .range([margin.left, width - margin.right])
    .padding(0.5)

  const yScale = d3.scaleLinear()
    .domain([0, d3.max(data, d => d.frequency)])
    .range([0, height])

  const xAxis = svg.append("g")
    .attr("class", "x-axis")

  const yAxis = svg.append("g")
    .attr("class", "y-axis")

  redraw(width, height);

  function redraw(width, height) 

    yScale.range([margin.top, height - margin.bottom])
  
    svg.selectAll(".y-axis")
      .attr("transform", `translate($margin.left,0)`)
      .call(d3.axisLeft(yScale)
        .ticks(data, d => d.frequency)
        .tickFormat(function(d, i) 
          return data[i].frequency;
          ));
  
    xScale.rangeRound([margin.left, width - margin.right]);

    svg.selectAll(".x-axis").transition().duration(0)
      .attr("transform", `translate(0,$height)`)
      .call(d3.axisBottom(xScale));
  
    var bar = svg.selectAll(".bar")
      .data(data)

    bar.exit().remove();

    bar.enter().append("rect")
      .attr("class", "bar")
      .style("fill", "steelblue")
      .merge(bar)
      // origin of each rect is at top left corner, so width goes to right
      // and height goes to bottom :)
      .style('transform', 'scale(1, -1)')
    .transition().duration(1000)
      .attr("width", xScale.bandwidth())
      .attr("height", d => yScale(d.frequency))
      .attr("y", -height)
      .attr("x", d => xScale(d.letter))
      .attr("transform", (d, i) => `translate($0,$0)`)
  

  d3.select(window).on('resize', function() 
    width = parseInt(svg.style("width")) - margin.left - margin.right,
    height = parseInt(svg.style("height")) - margin.top - margin.bottom;
    redraw(width, height);
  );
<!DOCTYPE html>
<html>
<head>
  <title>Bar Chart - redraw on window resize</title>

  <style>
    #chart 
      outline: 1px solid red;
      position: absolute;
      width: 95%;
      height: 95%;
      overflow: visible;
    
  </style>
</head>
<body>

<script type="text/javascript">
  var windowWidth = window.innerWidth;
  var windowHeight = window.innerHeight;
  console.log('viewport width is: '+ windowWidth + ' and viewport height is: ' + windowHeight + '. Resize the browser window to fire the resize event.');
</script>
  
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.5.0/d3.min.js"></script>
<svg id="chart"></svg>

<script src="./responsiveBarWindowWidth.js"></script>
</body>
</html>

这是您的图表,只是将#my_dataviz父级的硬编码值500px改为100vh,这允许svg响应父级容器的高度并调整宽度相应地。

Plunker:https://plnkr.co/edit/sBa6VmRH27xcgNiB?preview

100vh 的高度分配给父容器

<!DOCTYPE html>
<html>
  <head>
    <title></title>
    <script src="https://unpkg.com/vue"></script>
    <script src="https://d3js.org/d3.v6.js"></script>
    <style>
      .area 
        fill: url(#area-gradient);
        stroke-width: 0px;
      
      // changed from 500px:
      #my_dataviz 
        height: 100vh
      
    </style>
  </head>
  <body>
    <div id="app">
      <div class="page">
        <div class="">
          <div id="my_dataviz" ref="chart"></div>
        </div>
      </div>
    </div>

    <script>
      new Vue(
        el: '#app',
        data: 
          type: Array,
          required: true,
        ,
        mounted() 

          const minScale = 0,
                maxScale = 35;

          var data = [
            
              key: 'One',
              value: 33,
            ,
            
              key: 'Two',
              value: 30,
            ,
            
              key: 'Three',
              value: 37,
            ,
            
              key: 'Four',
              value: 28,
            ,
            
              key: 'Five',
              value: 25,
            ,
            
              key: 'Six',
              value: 15,
            ,
          ];

          console.log(this.$refs["chart"].clientHeight)

          // set the dimensions and margins of the graph
          var margin =  top: 20, right: 0, bottom: 30, left: 40 ,
            width =
              this.$refs["chart"].clientWidth - margin.left - margin.right,
            height =
              this.$refs["chart"].clientHeight - margin.top - margin.bottom;

          // set the ranges
          var x = d3.scaleBand().range([0, width]).padding(0.3);
          var y = d3.scaleLinear().range([height, 0]);

          // append the svg object to the body of the page
          // append a 'group' element to 'svg'
          // moves the 'group' element to the top left margin
          var svg = d3
            .select(this.$refs['chart'])
            .classed('svg-container', true)
            .append('svg')
            .attr('class', 'chart')
            .attr(
              'viewBox',
              `0 0 $width + margin.left + margin.right $
                height + margin.top + margin.bottom
              `
            )
            .attr('preserveAspectRatio', 'xMinYMin meet')
            .classed('svg-content-responsive', true)
            .append('g')
            .attr(
              'transform',
              'translate(' + margin.left + ',' + margin.top + ')'
            );

          // format the data
          data.forEach(function (d) 
            d.value = +d.value;
          );

          // Scale the range of the data in the domains
          x.domain(
            data.map(function (d) 
              return d.key;
            )
          );
          y.domain([minScale, maxScale]);

          //Add horizontal lines
          let oneFourth = (maxScale - minScale) / 4;

          svg
            .append('svg:line')
            .attr('x1', 0)
            .attr('x2', width)
            .attr('y1', y(oneFourth))
            .attr('y2', y(oneFourth))
            .style('stroke', 'gray');

          svg
            .append('svg:line')
            .attr('x1', 0)
            .attr('x2', width)
            .attr('y1', y(oneFourth * 2))
            .attr('y2', y(oneFourth * 2))
            .style('stroke', 'gray');

          svg
            .append('svg:line')
            .attr('x1', 0)
            .attr('x2', width)
            .attr('y1', y(oneFourth * 3))
            .attr('y2', y(oneFourth * 3))
            .style('stroke', 'gray');

          //Defenining the tooltip div
          let tooltip = d3
            .select('body')
            .append('div')
            .attr('class', 'tooltip')
            .style('position', 'absolute')
            .style('top', 0)
            .style('left', 0)
            .style('opacity', 0);

          // append the rectangles for the bar chart
          svg
            .selectAll('.bar')
            .data(data)
            .enter()
            .append('rect')
            .attr('class', 'bar')
            .attr('x', function (d) 
              return x(d.key);
            )
            .attr('width', x.bandwidth())
            .attr('y', function (d) 
              return y(d.value);
            )
            .attr('height', function (d) 

              console.log(height, y(d.value))
              return height - y(d.value);
            )
            .attr('fill', '#206BF3')
            .attr('rx', 5)
            .attr('ry', 5)
            .on('mouseover', (e, i) => 
              d3.select(e.currentTarget).style('fill', 'white');
              tooltip.transition().duration(500).style('opacity', 0.9);
              tooltip
                .html(
                  `<div><h1>$i.key $
                    this.year
                  </h1><p>$converter.addPointsToEveryThousand(
                    i.value
                  ) kWh</p></div>`
                )
                .style('left', e.pageX + 'px')
                .style('top', e.pageY - 28 + 'px');
            )
            .on('mouseout', (e) => 
              d3.select(e.currentTarget).style('fill', '#206BF3');
              tooltip.transition().duration(500).style('opacity', 0);
            );

          // Add the X Axis and styling it
          let xAxis = svg
            .append('g')
            .attr('transform', 'translate(0,' + height + ')')
            .call(d3.axisBottom(x));

          xAxis
            .select('.domain')
            .attr('stroke', 'gray')
            .attr('stroke-width', '3px');
          xAxis.selectAll('.tick text').attr('color', 'gray');
          xAxis.selectAll('.tick line').attr('stroke', 'gray');

          // add the y Axis and styling it also only show 0 and max tick
          let yAxis = svg.append('g').call(
            d3
              .axisLeft(y)
              .tickValues([this.minScale, this.maxScale])
              .tickFormat((d) => 
                if (d > 1000) 
                  d = Math.round(d / 1000);
                  d = d + 'K';
                
                return d;
              )
          );

          yAxis
            .select('.domain')
            .attr('stroke', 'gray')
            .attr('stroke-width', '3px');
          yAxis.selectAll('.tick text').attr('color', 'gray');
          yAxis.selectAll('.tick line').attr('stroke', 'gray');

          d3.select(window).on('resize', () => 
            svg.attr(
              'viewBox',
              `0 0 $this.$refs['chart'].clientWidth $this.$refs['chart'].clientHeight`
            );
          );
        ,
      );
    </script>
  </body>
</html>

【讨论】:

您好,感谢您的出色回答。我没有添加完整的代码,因为我无法让它在代码片段中运行。我已经用 sn-p 中的完整代码更新了这个问题,但我似乎无法让它运行。在我看来,您解释视口的答案部分与我在代码中使用的部分完全相同。所以我不明白为什么它不想在我的代码中工作。除了您使用边距,但我的图表不需要任何边距。边距由父 div 设置。 尝试将#my_dataviz 的高度设置为100vh,而不是硬编码值。 Plunker:plnkr.co/edit/sBa6VmRH27xcgNiB 我已经在我的浏览器中打开了你的代码,但它仍然不能适应这些变化。 SVG 的宽度随父容器的宽度而变化。但是 SVG 保持相同的纵横比,当有空间保持较大时,高度仍然会发生变化。 imgur.com/a/EWtm9kn 我想我需要使用通用的更新模式来同时完全适应宽度和高度。我正在尝试使用此模式更新我的示例代码。 再说一次,我不能在上面发表评论,但你是对的,使用 preserveAspectRatioxMinYmin meet 会导致你看到的行为。 imgur.com/HMlRJFZ 正如你所知道的,使用 SVG + resize 事件和想要控制 DOM 的库(如 Vue 和 React)可能会很棘手! :)

以上是关于D3 在调整大小窗口上更改 SVG 尺寸的主要内容,如果未能解决你的问题,请参考以下文章

d3.js Map (<svg>) 自动适应父容器并随窗口调整大小

D3 svg 组件不可调整大小

调整全屏世界地图 D3JS 的大小

调整包含 SVG (d3) 的 React 组件的大小

使用 JavaScript 更改 Highcharts 的高度。默认情况下,仅在窗口重新调整大小时重新调整大小

如何在容器尺寸的更改上调整Morris.js图表 的大小