【问题标题】:What's the best way to make a d3.js visualisation layout responsive?使 d3.js 可视化布局响应的最佳方法是什么?
【发布时间】:2019-05-21 17:17:23
【问题描述】:

假设我有一个构建 960 500 svg 图形的直方图脚本。如何使这个响应调整图形宽度和高度是动态的?

<script> 

var n = 10000, // number of trials
    m = 10,    // number of random variables
    data = [];

// Generate an Irwin-Hall distribution.
for (var i = 0; i < n; i++) {
  for (var s = 0, j = 0; j < m; j++) {
    s += Math.random();
  }
  data.push(s);
}

var histogram = d3.layout.histogram()
    (data);

var width = 960,
    height = 500;

var x = d3.scale.ordinal()
    .domain(histogram.map(function(d) { return d.x; }))
    .rangeRoundBands([0, width]);

var y = d3.scale.linear()
    .domain([0, d3.max(histogram.map(function(d) { return d.y; }))])
    .range([0, height]);

var svg = d3.select("body").append("svg")
    .attr("width", width)
    .attr("height", height);

svg.selectAll("rect")
    .data(histogram)
  .enter().append("rect")
    .attr("width", x.rangeBand())
    .attr("x", function(d) { return x(d.x); })
    .attr("y", function(d) { return height - y(d.y); })
    .attr("height", function(d) { return y(d.y); });

svg.append("line")
    .attr("x1", 0)
    .attr("x2", width)
    .attr("y1", height)
    .attr("y2", height);

</script> 

完整的直方图示例要点是: https://gist.github.com/993912

【问题讨论】:

标签: javascript responsive-design d3.js


【解决方案1】:

还有另一种不需要重绘图表的方法,它涉及修改&lt;svg&gt; 元素上的viewBoxpreserveAspectRatio 属性:

<svg id="chart" viewBox="0 0 960 500"
  preserveAspectRatio="xMidYMid meet">
</svg>

2015 年 11 月 24 日更新:大多数现代浏览器可以 infer the aspect ratio 来自 viewBox 的 SVG 元素,因此您可能不需要更新图表的大小。如果您需要支持旧版浏览器,您可以在窗口调整大小时调整元素大小,如下所示:

var aspect = width / height,
    chart = d3.select('#chart');
d3.select(window)
  .on("resize", function() {
    var targetWidth = chart.node().getBoundingClientRect().width;
    chart.attr("width", targetWidth);
    chart.attr("height", targetWidth / aspect);
  });

并且 svg 内容将自动缩放。你可以看到一个工作示例(有一些修改)here:只需调整窗口或右下窗格的大小,看看它是如何反应的。

【讨论】:

  • 我真的很喜欢这个方法 shawn,2 个问题。 1 视图框是否可以跨浏览器工作? 2. 在您的示例中,圆圈调整大小但不适合窗口。 svg 视图框或纵横比是否有问题。
  • 喜欢这种方法,不确定上述方法是否正确。正如马特所说,圆圈会重新调整大小,但从图表左侧到第一个圆圈的距离不会以与圆圈相同的速度调整大小。我尝试在 jsfiddle 中摆弄一点,但使用 Google Chrome 无法使其按预期工作。 viewBox 和 preserveAspectRatio 兼容哪些浏览器?
  • 实际上仅将 viewBox 和 preserveAspectRatio 设置为宽度高度设置为父级的 100% 的 SVG,并且父级是引导/弹性网格对我来说有效,无需处理调整大小事件
  • 确认Deepu是正确的。与 Bootstrap/flex 网格一起使用。谢谢@Deepu。
  • 注意:在几个 d3 教程中 1.) 在 html 中设置了 svg 的宽度和高度 2.) 可视化是通过 onload 函数调用进行的。如果在实际的 html 中设置了 svg 的宽度和高度并且您使用上述技术,则图像将无法缩放。
【解决方案2】:

这里有很多复杂的答案。

基本上,您需要做的就是放弃widthheight 属性,转而使用viewBox 属性:

width = 500;
height = 500;

const svg = d3
  .select("#chart")
  .append("svg")
  .attr("viewBox", `0 0 ${width} ${height}`)

如果您有边距,您可以将它们添加到宽度/高度中,然后在其后附加 g 并像往常一样对其进行转换。

【讨论】:

    【解决方案3】:

    如果您使用像 plottable.js 这样的 d3 包装器,请注意最简单的解决方案可能是添加事件侦听器,然后调用 redraw 函数(redraw 在 plottable.js 中)。在 plottable.js 的情况下,这将非常有效(这种方法的文档记录很差):

        window.addEventListener("resize", function() {
          table.redraw();
        });
    

    【讨论】:

    • 最好对这个函数进行去抖动以避免尽快触发重绘
    【解决方案4】:

    如果人们仍在访问这个问题 - 这对我有用:

    • 将 iframe 包含在一个 div 中,并使用 css 为该 div 添加一个 40% 的填充(百分比取决于您想要的纵横比)。然后将 iframe 本身的宽度和高度都设置为 100%。

    • 在包含要在 iframe 中加载的图表的 html 文档中,将 width 设置为 svg 附加到的 div 的宽度(或 body 的宽度),并将 height 设置为 width * aspect比例。

    • 编写一个函数,在窗口调整大小时重新加载 iframe 内容,以便在人们旋转手机时适应图表的大小。

    我的网站上的示例: http://dirkmjk.nl/en/2016/05/embedding-d3js-charts-responsive-website

    2016 年 12 月 30 日更新

    我上面描述的方法有一些缺点,特别是它没有考虑不属于 D3 创建的 svg 的任何标题和说明的高度。从那以后,我发现了一种我认为更好的方法:

    • 将 D3 图表的宽度设置为其附加的 div 的宽度,并使用纵横比相应地设置其高度;
    • 让嵌入页面使用 HTML5 的 postMessage 将其高度和 url 发送到父页面;
    • 在父页面上,使用 url 来识别相应的 iframe(如果您的页面上有多个 iframe,则很有用)并将其高度更新为嵌入页面的高度。

    我的网站上的示例:http://dirkmjk.nl/en/2016/12/embedding-d3js-charts-responsive-website-better-solution

    【讨论】:

      【解决方案5】:

      我会避免像瘟疫这样的调整大小/勾选解决方案,因为它们效率低下并且可能会导致您的应用出现问题(例如,工具提示会重新计算它应该在窗口调整大小时出现的位置,然后稍后您的图表也会调整大小并且页面重新布局,现在你的工具提示又错了)。

      您可以在一些旧的浏览器中模拟这种行为,这些浏览器也不能正确支持它,比如 IE11,使用 &lt;canvas&gt; 元素来保持它的外观。

      鉴于 960x540 是 16:9 的一个方面:

      <div style="position: relative">
        <canvas width="16" height="9" style="width: 100%"></canvas>
        <svg viewBox="0 0 960 540" preserveAspectRatio="xMidYMid meet" style="position: absolute; top: 0; right: 0; bottom: 0; left: 0; -webkit-tap-highlight-color: transparent;">
        </svg>
      </div>
      

      【讨论】:

        【解决方案6】:

        D3 数据连接的基本原则之一是它是幂等的。换句话说,如果您重复评估具有相同数据的数据连接,则呈现的输出是相同的。因此,只要您正确渲染图表,注意输入、更新和退出选择 - 当大小发生变化时,您所要做的就是重新渲染整个图表。

        您还应该做一些其他的事情,其中​​之一是消除窗口调整大小处理程序的抖动以限制它。此外,这应该通过测量包含元素来实现,而不是硬编码宽度/高度。

        作为替代方案,这是使用d3fc 呈现的图表,这是一组正确处理数据连接的 D3 组件。它还有一个笛卡尔图表,可以测量它包含的元素,从而可以轻松创建“响应式”图表:

        // create some test data
        var data = d3.range(50).map(function(d) {
          return {
            x: d / 4,
            y: Math.sin(d / 4),
            z: Math.cos(d / 4) * 0.7
          };
        });
        
        var yExtent = fc.extentLinear()
          .accessors([
            function(d) { return d.y; },
            function(d) { return d.z; }
          ])
          .pad([0.4, 0.4])
          .padUnit('domain');
        
        var xExtent = fc.extentLinear()
          .accessors([function(d) { return d.x; }]);
        
        // create a chart
        var chart = fc.chartSvgCartesian(
            d3.scaleLinear(),
            d3.scaleLinear())
          .yDomain(yExtent(data))
          .yLabel('Sine / Cosine')
          .yOrient('left')
          .xDomain(xExtent(data))
          .xLabel('Value')
          .chartLabel('Sine/Cosine Line/Area Chart');
        
        // create a pair of series and some gridlines
        var sinLine = fc.seriesSvgLine()
          .crossValue(function(d) { return d.x; })
          .mainValue(function(d) { return d.y; })
          .decorate(function(selection) {
            selection.enter()
              .style('stroke', 'purple');
          });
        
        var cosLine = fc.seriesSvgArea()
          .crossValue(function(d) { return d.x; })
          .mainValue(function(d) { return d.z; })
          .decorate(function(selection) {
            selection.enter()
              .style('fill', 'lightgreen')
              .style('fill-opacity', 0.5);
          });
        
        var gridlines = fc.annotationSvgGridline();
        
        // combine using a multi-series
        var multi = fc.seriesSvgMulti()
          .series([gridlines, sinLine, cosLine]);
        
        chart.plotArea(multi);
        
        // render
        d3.select('#simple-chart')
          .datum(data)
          .call(chart);
        

        您可以在此代码笔中看到它的实际效果:

        https://codepen.io/ColinEberhardt/pen/dOBvOy

        您可以在其中调整窗口大小并验证图表是否正确重新呈现。

        请注意,作为一个完整的披露,我是 d3fc 的维护者之一。

        【讨论】:

        • 感谢您及时更新您的答案!我已经看到了您的一些帖子的最新更新,我真的很感谢人们重新访问他们自己的内容!但是,请注意,此修订版不再完全适合该问题,因为您在 D3 v4 中进行编辑,而 OP 显然指的是 v3,这可能会让未来的读者感到困惑。就我个人而言,对于我自己的答案,我倾向于添加一个 v4 部分,同时仍保留原始 v3 部分。我认为 v4 可能非常值得自己的答案添加对两个帖子的相互引用。但这只是我的两分钱......
        • 这是一个非常好的观点!很容易忘乎所以,只关注未来和新事物。从最新的 d3 问题来看,我认为很多人仍在使用 v3。一些差异是多么微妙也令人沮丧!我会重新审视我的编辑:-)
        【解决方案7】:

        不使用 ViewBox

        这是一个不依赖于使用viewBox 的解决方案示例:

        关键在于更新用于放置数据的刻度范围

        首先,计算你的原始纵横比:

        var ratio = width / height;
        

        然后,在每次调整大小时,更新 xyrange

        function resize() {
          x.rangeRoundBands([0, window.innerWidth]);
          y.range([0, window.innerWidth / ratio]);
          svg.attr("height", window.innerHeight);
        }
        

        请注意,高度是基于宽度和纵横比的,因此您的原始比例保持不变。

        最后,“重绘”图表——更新任何依赖于xy 比例尺的属性:

        function redraw() {
            rects.attr("width", x.rangeBand())
              .attr("x", function(d) { return x(d.x); })
              .attr("y", function(d) { return y.range()[1] - y(d.y); })
              .attr("height", function(d) { return y(d.y); });
        }
        

        请注意,在调整rects 的大小时,您可以使用yrange 的上限,而不是显式使用高度:

        .attr("y", function(d) { return y.range()[1] - y(d.y); })
        

        var n = 10000, // number of trials
          m = 10, // number of random variables
          data = [];
        
        // Generate an Irwin-Hall distribution.
        for (var i = 0; i < n; i++) {
          for (var s = 0, j = 0; j < m; j++) {
            s += Math.random();
          }
          data.push(s);
        }
        
        var histogram = d3.layout.histogram()
          (data);
        
        var width = 960,
          height = 500;
        
        var ratio = width / height;
        
        var x = d3.scale.ordinal()
          .domain(histogram.map(function(d) {
            return d.x;
          }))
        
        var y = d3.scale.linear()
          .domain([0, d3.max(histogram, function(d) {
            return d.y;
          })])
        
        var svg = d3.select("body").append("svg")
          .attr("width", "100%")
          .attr("height", height);
        
        var rects = svg.selectAll("rect").data(histogram);
        rects.enter().append("rect");
        
        function redraw() {
          rects.attr("width", x.rangeBand())
            .attr("x", function(d) {
              return x(d.x);
            })
            // .attr("y", function(d) { return height - y(d.y); })
            .attr("y", function(d) {
              return y.range()[1] - y(d.y);
            })
            .attr("height", function(d) {
              return y(d.y);
            });
        }
        
        function resize() {
          x.rangeRoundBands([0, window.innerWidth]);
          y.range([0, window.innerWidth / ratio]);
          svg.attr("height", window.innerHeight);
        }
        
        d3.select(window).on('resize', function() {
          resize();
          redraw();
        })
        
        resize();
        redraw();
        &lt;script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"&gt;&lt;/script&gt;

        【讨论】:

          【解决方案8】:

          如果您通过c3.js 使用 d3.js,响应性问题的解决方案非常简单:

          var chart = c3.generate({bindTo:"#chart",...});
          chart.resize($("#chart").width(),$("#chart").height());
          

          生成的 HTML 的样子:

          <div id="chart">
              <svg>...</svg>
          </div>
          

          【讨论】:

            【解决方案9】:

            Shawn Allen 的回答很棒。但是您可能不想每次都这样做。如果您在 vida.io 上托管它,您将自动响应您的 svg 可视化。

            您可以使用这个简单的嵌入代码获得响应式 iframe:

            <div id="vida-embed">
            <iframe src="http://embed.vida.io/documents/9Pst6wmB83BgRZXgx" width="auto" height="525" seamless frameBorder="0" scrolling="no"></iframe>
            </div>
            
            #vida-embed iframe {
              position: absolute;
              top:0;
              left: 0;
              width: 100%;
              height: 100%;
            }
            

            http://jsfiddle.net/dnprock/npxp3v9d/1/

            披露:我在 vida.io 构建此功能。

            【讨论】:

              【解决方案10】:

              寻找“响应式 SVG” 让 SVG 响应式变得非常简单,您不必再担心尺寸问题。

              我是这样做的:

              d3.select("div#chartId")
                 .append("div")
                 .classed("svg-container", true) //container class to make it responsive
                 .append("svg")
                 //responsive SVG needs these 2 attributes and no width and height attr
                 .attr("preserveAspectRatio", "xMinYMin meet")
                 .attr("viewBox", "0 0 600 400")
                 //class to make it responsive
                 .classed("svg-content-responsive", true); 
              

              CSS 代码:

              .svg-container {
                  display: inline-block;
                  position: relative;
                  width: 100%;
                  padding-bottom: 100%; /* aspect ratio */
                  vertical-align: top;
                  overflow: hidden;
              }
              .svg-content-responsive {
                  display: inline-block;
                  position: absolute;
                  top: 10px;
                  left: 0;
              }
              

              更多信息/教程:

              http://demosthenes.info/blog/744/Make-SVG-Responsive

              http://soqr.fr/testsvg/embed-svg-liquid-layout-responsive-web-design.php

              【讨论】:

              • 就我而言, padding-bottom: 100% 将使我的容器底部有额外的填充,因此我将其更改为 50% 以将其删除。使用您的代码并成功使 svg 响应,谢谢。
              【解决方案11】:

              您还可以使用 bootstrap 3 来调整可视化的大小。例如,我们可以将 HTML 代码设置为:

              <div class="container>
              <div class="row">
              
              <div class='col-sm-6 col-md-4' id="month-view" style="height:345px;">
              <div id ="responsivetext">Something to write</div>
              </div>
              
              </div>
              </div>
              

              由于我的需要,我设置了固定高度,但您也可以将尺寸设为自动。 “col-sm-6 col-md-4”使 div 响应不同的设备。您可以通过http://getbootstrap.com/css/#grid-example-basic了解更多信息

              我们可以借助 ID month-view 访问图表。

              d3代码我就不赘述了,我只输入适应不同屏幕尺寸需要的部分。

              var width = document.getElementById('month-view').offsetWidth;
              
              var height = document.getElementById('month-view').offsetHeight - document.getElementById('responsivetext2').offsetHeight;
              

              通过获取id为month-view的div的宽度来设置宽度。

              在我的情况下,高度不应包括整个区域。我在栏上方也有一些文字,所以我也需要计算该区域。这就是为什么我用 id responsivetext 来标识文本区域的原因。为了计算栏的允许高度,我从 div 的高度中减去了文本的高度。

              这允许您拥有一个可以采用所有不同屏幕/div 大小的栏。这可能不是最好的方法,但它确实可以满足我项目的需求。

              【讨论】:

              • 嗨,我试过了,但是 svg 内容没有像日历或图表那样调整大小。
              【解决方案12】:

              我编写了一个小要点来解决这个问题。

              一般的解决模式是这样的:

              1. 将脚本拆分为计算和绘图函数。
              2. 确保绘图函数动态绘制并由 可视化宽度和高度变量(最好的方法是 使用 d3.scale api)
              3. 将绘图绑定/链接到参考 标记中的元素。 (我为此使用了 jquery,因此将其导入)。
              4. 如果它已经绘制,请记住将其删除。从中获取尺寸 使用 jquery 的引用元素。
              5. 将绘图函数绑定/链接到 窗口调整大小功能。对此引入去抖动(超时) 链以确保我们仅在超时后重绘。

              我还添加了缩小的 d3.js 脚本以提高速度。 要点在这里:https://gist.github.com/2414111

              jquery 参考返回代码:

              $(reference).empty()
              var width = $(reference).width();
              

              去抖代码:

              var debounce = function(fn, timeout) 
              {
                var timeoutID = -1;
                return function() {
                   if (timeoutID > -1) {
                      window.clearTimeout(timeoutID);
                   }
                 timeoutID = window.setTimeout(fn, timeout);
                }
              };
              
              var debounced_draw = debounce(function() {
                  draw_histogram(div_name, pos_data, neg_data);
                }, 125);
              
               $(window).resize(debounced_draw);
              

              享受吧!

              【讨论】:

              • 这似乎不起作用;如果我调整包含在更正要点中的 html 文件的窗口大小,直方图仍然会随着窗口变小而被截断......
              • SVG 是一种矢量格式。您可以设置任何虚拟视图框大小,然后将其缩放到实际尺寸。无需使用JS。
              猜你喜欢
              • 1970-01-01
              • 2016-11-24
              • 2017-11-29
              • 2021-04-13
              • 1970-01-01
              相关资源
              最近更新 更多