>假设我有一个构建 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
我已经编写了一个小要点来解决这个问题。
一般解决方案模式如下:
- 将脚本分解为计算函数和绘图函数。
- 确保绘图功能动态绘制并驱动可视化宽度和高度变量(最好的方法是使用 d3.scale API(
- 将图形绑定/链接到参照元素。(我为此使用了jquery,所以导入了它(。
- 如果已经绘制,请记住将其删除。从以下位置获取维度使用 jQuery 的引用元素。
- 将绘制功能绑定/链接到窗口调整大小功能。为此引入去抖动(超时(链,以确保我们仅在超时后重新绘制。
我还添加了缩小的 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;
然后,在每次调整大小时,更新x
和y
的range
:
function resize() {
x.rangeRoundBands([0, window.innerWidth]);
y.range([0, window.innerWidth / ratio]);
svg.attr("height", window.innerHeight);
}
请注意,高度基于宽度和纵横比,因此可以保持原始比例。
最后,"重绘"图表 – 更新依赖于x
或y
比例的任何属性:
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>
这里有很多复杂的答案。
基本上,您需要做的就是放弃width
和height
属性,转而使用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 尺寸的栏。这可能不是最好的方法,但它肯定适合我的项目需求。