域更改后平移缩放

时间:2020-10-04 09:51:52

标签: javascript d3.js

我有一些时间序列数据,其域发生了变化:我可以使用最近的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" />

2 个答案:

答案 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>

编辑:我现在可以更好地理解您的问题。我在下面所做的事情如下:

  1. 每单击一个按钮,首先获取缩放的域xz;
  2. 然后查看是否需要对其进行钳位,以便域是新数据的子集。 xz.domain()必须始终在x.domain()范围内;
  3. 如果是这种情况,请计算缩放比例和视口中心的点;
  4. 完全重绘图表;
  5. 使用先前计算的比例尺,将d3缩放为正确的比例,然后使用先前计算的中心点将其平移到正确的位置。

此外,我将y域更改为始终使用整个数据集进行计算。这样可以确保当按下任何按钮时,线条不会垂直跳动。

除非您的视口包含单击按钮后不再可用的数据,否则不会绕x轴跳动。

测试用例

所有这些视图都应保持不变:

  • 点击“去年”,然后点击“ 2年”;
  • 单击“ 2年”,然后放大到2020年1月1日。单击“ 6个月”;
  • 单击“去年”,缩放并平移直到其涵盖2019年2月至4月。单击“ 2年”;
  • 单击“ 6个月”,然后单击“去年”,然后单击“ 2年”。

所有这些视图都应改变:

  • 点击“ 2年”,完全缩小,然后“ 6个月”;
  • 单击“ 2年”,然后放大到2020年2月1日。单击“ 6个月”;
  • 单击“去年”,缩放并平移直到其涵盖2019年2月至4月。单击“ 6个月”。

// 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>