我有一些时间序列数据,其域发生了变化:我可以使用最近的6个月,去年,最近的2年,依此类推。我创建了一个仅显示数据的D3图表。
但是,您也可以缩放此图表,但是当缩放然后更改域时,缩放“重置”,但是单击后再次起作用。
当域更改时,我想保持当前缩放:由于它是时间序列数据,所以我希望它位于同一位置。我该怎么做?
<Spinner
android:id="@+id/spinnerAmount"
android:layout_width="0dp"
android:layout_height="28dp"
android:layout_marginStart="8dp"
android:background="@drawable/spinner_background"
android:singleLine="true"
android:spinnerMode="dialog"
app:layout_constraintBottom_toBottomOf="@+id/editTextNumber"
app:layout_constraintStart_toEndOf="@+id/buttonAdd"
app:layout_constraintTop_toTopOf="@+id/editTextNumber" />
答案 0 :(得分:1)
问题
如果您使用d3.zoom缩放/平移,则需要在手动进行并更改了平移/缩放时告知d3.zoom。它不会“知道”您在外部进行了何种篡改。此外,如果要更新元素的缩放状态以使d3.zoom知道更改,那么为什么不也使用d3.zoom进行缩放呢?
在您的示例中,您使用缩放来设置数据的比例,但是当您单击按钮时,仅通过过滤数据来设置缩放。 d3.zoom绝非明智之举。这就是为什么当您使用按钮然后进行缩放时会发生跳跃的原因-缩放行为会在最后一次离开的地方开始。
最后,当您可以全部通过d3.zoom运行时,您已经编写了两种缩放和平移方法。
这不是一个罕见的问题-here是一个发挥相同原理的示例。
解决方案
仅使用一种方法进行缩放/平移。这样,就无需同步两个单独的缩放/平移机制的行为和状态。您可以很容易地将d3.zoom用于programmatic zooms和标准缩放。
在处理坐标轴和比例尺时,您会发现使用参考比例尺最容易-这种方式缩放是相对于原始缩放状态而不是最后一个缩放状态(这可能会导致problems)。我们在每次缩放事件时都使用参考比例来调整我们的工作比例。工作刻度将传递到轴生成器并用于定位数据。
因此,在您的情况下,我们的缩放功能看起来就像:
const zoomed = (event) => {
xScale.domain(event.transform.rescaleX(xReference).domain());
draw(data);
}
我们每次都重新缩放xScale,以反映出zoom事件提供的zoom变换所显示的新域。
这适用于鼠标交互,无需进一步修改。我们可以使用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>
答案 1 :(得分:0)
一种方法是将上一个缩放事件存储在一个变量中,如果存在,则不要从头开始重画轴,而用最后一个事件调用zoomed()
。 / s>
编辑:我现在可以更好地理解您的问题。我在下面所做的事情如下:
xz
; xz.domain()
必须始终在x.domain()
范围内; d3
缩放为正确的比例,然后使用先前计算的中心点将其平移到正确的位置。此外,我将y
域更改为始终使用整个数据集进行计算。这样可以确保当按下任何按钮时,线条不会垂直跳动。
除非您的视口包含单击按钮后不再可用的数据,否则不会绕x
轴跳动。
测试用例
所有这些视图都应保持不变:
所有这些视图都应改变:
// 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 = 400
const width = 800
const margin = {
top: 20,
right: 0,
bottom: 30,
left: 40
}
let x;
let y;
let xz;
const zoomed = (event) => {
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('width', width)
.attr('height', 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])
let reScale = false,
domain,
centerPoint;
if(xz !== undefined) {
domain = xz.domain();
centerPoint = xz.invert((width - margin.left - margin.right) / 2);
// If the previous center completely falls out of the current bounds, draw the chart anew.
if(domain[1] < data[0].date || domain[0] > data[data.length - 1].date) {
// Nothing
} else {
// Else, clip the domain to fit the data.
if(domain[0] < data[0].date) {
domain[0] = data[0].date;
}
if(domain[1] > data[data.length - 1].date) {
domain[1] = data[data.length - 1].date;
}
reScale = true;
}
}
gY.call(yAxis, y);
gX.call(xAxis, x);
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)))
if(reScale) {
const scale = (x.domain()[1] - x.domain()[0])/(domain[1] - domain[0]);
svg.call(zoom.scaleTo, scale)
.call(zoom.translateTo, centerPoint, 0);
}
}
// Buttons
const data = randomData()
// To avoid jumpy behaviour, make sure the y-domain is steady
y = d3.scaleLinear()
.domain(d3.extent(data, d => d.value)).nice()
.range([height - margin.bottom, margin.top])
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 src="https://d3js.org/d3.v6.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>