javascript - 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>
解决方案
使用 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>