使 d3.js 可视化布局响应的最佳方法是什么?

What's the best way to make a d3.js visualisation layout responsive?

本文关键字:最佳 方法 是什么 响应 d3 js 可视化 布局      更新时间:2023-09-26

>假设我有一个构建 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

还有

另一种方法不需要重绘图形,它涉及修改 viewBox 和 <svg> 元素上的 preserveAspectRatio 属性:

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

11/24/15 更新:大多数现代浏览器都可以从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 内容将自动缩放。您可以在此处看到一个工作示例(经过一些修改(:只需调整窗口或右下窗格的大小即可查看其反应。

寻找"响应式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

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

一般解决方案模式如下:

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

享受!

不使用 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的大小时,您可以使用 y range的上限,而不是显式使用高度:

.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();
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

这里有很多复杂的答案。

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

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

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

如果您使用的是 d3.js 到 c3.js则响应能力问题的解决方案非常简单:

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

其中生成的 HTML 如下所示:

<div id="chart">
    <svg>...</svg>
</div>
如果您

使用的是 d3 包装器(如 plottable.js(,请注意最简单的解决方案可能是添加事件侦听器,然后调用重绘函数(redraw in plottable.js(。在可绘图的情况下.js这将非常有效(这种方法的文档很少(:

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

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 构建此功能。

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

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

  • 在包含要在 iframe 中加载的图表的 html 文档中,将宽度设置为 svg 附加到的div 的宽度(或正文的宽度(,并设置高度与宽度 * 纵横比。

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

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

更新 30 Dec 2016

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

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

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

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

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

作为替代方法,下面是使用 d3fc 呈现的图表,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的维护者之一。

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

您可以在一些不能正确支持它的旧浏览器中模拟这种行为,例如 IE11,也可以使用 <canvas> 元素来维护它的外观。

给定 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>

您还可以使用 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 月视图访问图表。

不会详细介绍 d3 代码,我只会输入适应不同屏幕尺寸所需的部分。

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

宽度是通过获取带有 id 月视图的div 的宽度来设置的。

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

这允许您拥有一个采用所有不同屏幕/div 尺寸的栏。这可能不是最好的方法,但它肯定适合我的项目需求。