javascript - 域更改后平移缩放
问题描述
我有一些时间序列数据的域发生变化:我可以使用过去 6 个月、去年、过去 2 年等等。我创建了一个仅显示数据的 D3 图表。
但是,您也可以缩放此图表,但是当您缩放然后更改域时,缩放“重置”但在您单击时再次起作用。
当域发生变化时,我想保持当前的缩放:因为它是时间序列数据,我希望它在同一个地方。我怎样才能做到这一点?
<head>
<script src="https://d3js.org/d3.v6.min.js"></script>
<div class="buttons">
<button id="sixmo">Last 6 months</button>
<button id="oneyear">Last year</button>
<button id="twoyears">Last 2 years</button>
</div>
</head>
<body>
<script>
// Random data
function randomData() {
function randn_bm() {
var u = 0, v = 0;
while (u === 0) u = Math.random();
while (v === 0) v = Math.random();
return Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
}
let days = []
let endDate = new Date(2020, 1, 0)
for (var d = new Date(2018, 0, 0); d <= endDate; d.setDate(d.getDate() + 1)) {
days.push(new Date(d));
}
return days.map(d => ({
date: d,
value: randn_bm()
}))
}
// Chart
const height = 600
const width = 800
const margin = { top: 20, right: 0, bottom: 30, left: 40 }
let x;
let y;
const zoomed = (event) => {
let xz = event.transform.rescaleX(x);
gX.call(xAxis, xz);
gLine.selectAll("path")
.data([data])
.join("path")
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("stroke-width", 1.5)
.attr("d", d3.line()
.x(d => xz(d.date))
.y(d => y(d.value)))
}
const zoom = d3.zoom()
.scaleExtent([1, 32])
.extent([[margin.left, 0], [width - margin.right, height]])
.translateExtent([[margin.left, -Infinity], [width - margin.right, Infinity]])
.on("zoom", zoomed);
const svg = d3.select("body").append("svg")
.attr("viewBox", [0, 0, width, height]);
svg.call(zoom)
const gLine = svg.append("g").attr("class", "series").attr("clip-path", "url(#clip)")
const gX = svg.append("g").attr("class", "x-axis")
const gY = svg.append("g").attr("class", "y-axis")
const xAxis = (g, x) => g
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(x).tickSizeOuter(0))
const yAxis = (g, y) => g
.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(y))
.call(g => g.select(".domain").remove())
svg.append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("x", margin.left)
.attr("y", margin.top)
.attr("width", width - margin.left - margin.right)
.attr("height", height - margin.top - margin.bottom);
function renderChart(data) {
x = d3.scaleTime()
.domain(d3.extent(data, d => d.date))
.range([margin.left, width - margin.right])
y = d3.scaleLinear()
.domain(d3.extent(data, d => d.value)).nice()
.range([height - margin.bottom, margin.top])
gX.call(xAxis, x);
gY.call(yAxis, y);
gLine.selectAll("path")
.data([data])
.join("path")
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("stroke-width", 1.5)
.attr("d", d3.line()
.x(d => x(d.date))
.y(d => y(d.value)))
}
// Buttons
const data = randomData()
const lastDataDate = new Date(2020, 1, 0)
const buttons = d3.select(".buttons")
.selectAll("button")
.data([6, 12, 24])
.join("button")
.on("click", (_, months) => {
const startDate = new Date(lastDataDate)
startDate.setMonth(startDate.getMonth() - months)
const filteredData = data.filter(d => d.date > startDate)
renderChart(filteredData)
})
renderChart(data)
</script>
</body>
解决方案
问题
如果您使用 d3.zoom 进行缩放/平移,则需要让 d3.zoom 知道您何时手动进行并更改了平移/缩放。它不“知道”你在它之外做了什么样的篡改。此外,如果您要更新元素的缩放状态以便 d3.zoom “知道”更改,为什么不使用 d3.zoom 来实际进行缩放和平移呢?
在您的示例中,您使用缩放来设置数据的比例,但是当您单击按钮时,您仅通过过滤数据来设置缩放。d3.zoom 一点也不聪明。这就是为什么当您使用按钮然后使用缩放时会发生跳跃的原因 - 缩放行为会从上次离开的地方开始。
最后,您已经编写了两种缩放和平移方法,您可以通过 d3.zoom 来运行它们。
这不是一个不常见的问题 - 这是一个相同原理的示例。
解决方案
仅使用一种方法进行缩放/平移。这样就无需同步缩放/平移的两个独立机制的行为和状态。您可以很容易地将 d3.zoom 用于程序化缩放和标准缩放。
在处理轴和刻度时,您会发现使用参考刻度最容易 - 这样缩放是相对于原始缩放状态而不是最后一个缩放状态(这可能会导致问题)。我们使用每个缩放事件的参考比例来重新调整我们的工作比例。工作标尺被传递到轴生成器并用于定位数据。
因此,在您的情况下,我们的缩放功能看起来像:
const zoomed = (event) => {
xScale.domain(event.transform.rescaleX(xReference).domain());
draw(data);
}
我们每次都重新缩放 xScale 以反映缩放事件提供的缩放变换所显示的新域。
这适用于鼠标交互,无需进一步修改。我们可以使用 调用程序化缩放svg.call(zoom.transform, someZoomTransform)
,我们所要做的就是计算正确的变换,以您的代码为例,它看起来像:
const endDate = lastDataDate;
const startDate = d3.timeMonth.offset(endDate,-months);
// k = width of range needed for data set / width of range needed for area of interest
const k = (xReference.range()[1] - xReference.range()[0]) / (xReference(endDate) - xReference(startDate))\
// translate to account for starting point of area of interest.
const tx = xReference(startDate);
// let the zoom handle it.
svg.call(zoom.transform, d3.zoomIdentity
.scale(k)
.translate(-tx+margin.left/k, 0) // margin.left/k : account for scale range not starting at 0.
);
综上所述,我们得到:
const height = 500;
const width = 500;
const margin = { top: 20, right: 0, bottom: 30, left: 40 }
const svg = d3.select("body").append("svg")
.attr("width",width)
.attr("height",height);
var data = randomData();
// Set up Scales:
let xScale = d3.scaleTime()
.domain(d3.extent(data, d => d.date))
.range([margin.left, width - margin.right])
// Reference to hold starting version of scale:
const xReference = xScale.copy();
let yScale = d3.scaleLinear()
.domain(d3.extent(data, d => d.value)).nice()
.range([height - margin.bottom, margin.top])
// Set up Zoom:
const zoomed = (event) => {
xScale.domain(event.transform.rescaleX(xReference).domain());
draw(data);
}
const zoom = d3.zoom()
.scaleExtent([1, 32])
.extent([[margin.left, 0], [width - margin.right, height]])
.translateExtent([[margin.left, -Infinity], [width - margin.right, Infinity]])
.on("zoom", zoomed);
svg.call(zoom);
// Set up axes and miscellania
const gLine = svg.append("g").attr("class", "series").attr("clip-path", "url(#clip)")
const gX = svg.append("g").attr("class", "x-axis")
const gY = svg.append("g").attr("class", "y-axis")
const xAxis = (g, x) => g
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(xScale).tickSizeOuter(0))
const yAxis = (g, y) => g
.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(yScale))
.call(g => g.select(".domain").remove())
svg.append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("x", margin.left)
.attr("y", margin.top)
.attr("width", width - margin.left - margin.right)
.attr("height", height - margin.top - margin.bottom);
// Draw:
function draw(data) {
gX.call(xAxis, xScale);
gY.call(yAxis, yScale);
gLine.selectAll("path")
.data([data])
.join("path")
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("stroke-width", 1.5)
.attr("d", d3.line()
.x(d => xScale(d.date))
.y(d => yScale(d.value)))
}
// Button Behavior
const lastDataDate = new Date(2020, 1, 0)
const buttons = d3.select(".buttons")
.selectAll("button")
.data([6, 12, 24])
.join("button")
.on("click", (_, months) => {
const endDate = lastDataDate;
const startDate = d3.timeMonth.offset(endDate,-months);
// k = width of range needed for data set / width of range needed for area of interest
const k = (xReference.range()[1] - xReference.range()[0]) / (xReference(endDate) - xReference(startDate))
// translate to account for starting point of area of interest.
const tx = xReference(startDate);
// let the zoom handle it.
svg.call(zoom.transform, d3.zoomIdentity
.scale(k)
.translate(-tx+margin.left/k, 0) // account for scale range not starting at 0.
);
})
draw(data);
// Random data
function randomData() {
function randn_bm() {
var u = 0, v = 0;
while (u === 0) u = Math.random();
while (v === 0) v = Math.random();
return Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
}
let days = []
let endDate = new Date(2020, 1, 0)
for (var d = new Date(2018, 0, 0); d <= endDate; d.setDate(d.getDate() + 1)) {
days.push(new Date(d));
}
return days.map(d => ({
date: d,
value: randn_bm()
}))
}
<script src="https://d3js.org/d3.v6.min.js"></script>
<div class="buttons">
<button id="sixmo">Last 6 months</button>
<button id="oneyear">Last year</button>
<button id="twoyears">Last 2 years</button>
</div>
推荐阅读
- npm - Pupeteer - 如何自动接受任何 URL 的 cookie 同意提示?
- r - 如何在条形图中绘制垂直线?
- react-native - 传递更新状态 React Native
- php - 学生和班级的 SQL 语句 JOIN
- python - 如何在 python 中从 UI 设置对象名称和参数
- c++ - 使用 execv 从 C++ 代码执行的 Linux 脚本失败
- sql - 所选 mysql 查询中的变量
- swiftui - SwiftUI,NavigationView 在旋转时消失?
- excel - 如何通过 Excel 中的工作表索引号将多个工作表中的相同单元格相加?
- asp.net-core - Razor Pages - 如何将选择下拉值插入 asp-route?