首页 > 解决方案 > D3 在调整大小窗口上更改 SVG 尺寸

问题描述

我有一个项目,我正在使用 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>

标签: javascriptd3.jsresponsive

解决方案


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

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

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

在此处输入图像描述

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

Sara Soueidan 的文章Understanding SVG Coordinate Systems非常好。Curran Kelleher 的示例在这里使用通用更新模式来处理更惯用的东西。

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

强制 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>


推荐阅读