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
我想我需要使用通用的更新模式来同时完全适应宽度和高度。我正在尝试使用此模式更新我的示例代码。
再说一次,我不能在上面发表评论,但你是对的,使用 preserveAspectRatio
的 xMinYmin meet
会导致你看到的行为。 imgur.com/HMlRJFZ 正如你所知道的,使用 SVG + resize 事件和想要控制 DOM 的库(如 Vue 和 React)可能会很棘手! :)以上是关于D3 在调整大小窗口上更改 SVG 尺寸的主要内容,如果未能解决你的问题,请参考以下文章
d3.js Map (<svg>) 自动适应父容器并随窗口调整大小