具有可重用模式的D3语义缩放 [英] D3 semantic zooming with Reusable Pattern

查看:65
本文介绍了具有可重用模式的D3语义缩放的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试使用Mike Bostock的转向可重用图表模式来实现语义缩放(其中图表表示为函数).在我的缩放处理程序中,我想使用 transform .rescaleX来更新缩放比例,然后再次调用该函数即可.

几乎可以使用,但重新缩放似乎会累积越来越快的缩放变换.这是我的小提琴:

 功能chart(){让AspectRatio = 10.33;let margin = {顶部:0,右侧:0,底部:5,左侧:0};让current = new Date();让scaleBand = d3.scaleBand().padding(.2);让scaleTime = d3.scaleTime().domain([d3.timeDay(current),d3.timeDay.ceil(current)]);让axis = d3.axisBottom(scaleTime);let daysThisMonth = d3.timeDay.count(d3.timeMonth(current),d3.timeMonth.ceil(current));让clipTypes = [ClipType.Scheduled,ClipType.Alarm,ClipType.Motion];让zoom = d3.zoom().scaleExtent([1/daysThisMonth,1440]);让结果=功能(选择){selection.each(function(data){让selection = d3.select(this);让externalWidth = this.getBoundingClientRect().width;让outerHeight = externalWidth/aspectRatio;让width = externalWidth-margin.left-margin.right;设高度= externalHeight-margin.top-margin.bottom;scaleBand.domain(d3.range(data.length)).range([0,height * .8]);scaleTime.range([0,width]);zoom.on('zoom',_ => {scaleTime = d3.event.transform.rescaleX(scaleTime);selection.call(结果);});让svg = selection.selectAll('svg').data([data]);让svgEnter = svg.enter().append('svg').attr('viewBox','0 0'+ externalWidth +''+ externalHeight);//.attr('preserveAspectRatio','xMidYMin slice');svg = svg.merge(svgEnter);let defsEnter = svgEnter.append('defs');let defs = svg.select('defs');让gMainEnter = svgEnter.append('g').attr('id','main');让gMain = svg.select('g#main').attr('transform','translate('+ margin.left +''+ margin.top +')');让gAxisEnter = gMainEnter.append('g').attr('id','axis');让gAxis = gMain.select('g#axis').call(axis.scale(scaleTime));让gCameraContainerEnter = gMainEnter.append('g').attr('id','camera-container');让gCameraContainer = gMain.select('g#camera-container').attr('transform','translate('+ 0 +''+ height * .2 +')').call(zoom);让gCameraRowsEnter = gCameraContainerEnter.append('g').attr('id','camera-rows');让gCameraRows = gCameraContainer.select('g#camera-rows');让gCameras = gCameraRows.selectAll('g.camera').data(d => {返回d;});让gCamerasEnter = gCameras.enter().append('g').attr('class','camera');gCameras = gCameras.merge(gCamerasEnter);gCameras.exit().remove();让rectClips = gCameras.selectAll('rect.clip').data(d => {返回d.clips.filter(clip => {return clipTypes.indexOf(clip.type)!== -1;});});让rectClipsEnter = rectClips.enter().append('rect').attr('class','clip').attr('height',_ => {返回scaleBand.bandwidth();}).attr('y',(d,i,g)=> {返回scaleBand(Array.prototype.indexOf.call(g [i] .parentNode.parentNode.childNodes,g [i] .parentNode));//TODO:草率}).style('fill',d => {开关(d.type){大小写ClipType.Scheduled:返回'#0F0';大小写ClipType.Alarm:返回'#FF0';大小写ClipType.Motion:返回'#F00';};});rectClips = rectClips.merge(rectClipsEnter).attr('width',d => {返回scaleTime(d.endTime)-scaleTime(d.startTime);}).attr('x',d => {返回scaleTime(d.startTime);});rectClips.exit().remove();让rectBehaviorEnter = gCameraContainerEnter.append('rect').attr('id','behavior').style('fill','#000').style('opacity',0);让rectBehavior = gCameraContainer.select('rect#behavior').attr('width',width).attr('height',height * .8);//.call(zoom);});};返回结果;}//数据模型让ClipType = {预定:0,警报:1运动:2};让数据= [{编号:1src:"assets/1.jpg",名称:"Camera 1",服务器:1},{编号:2src:"assets/2.jpg",名称:"Camera 2",服务器:1},{id:3,src:"assets/1.jpg",名称:"3号相机",服务器:2},{编号:4src:"assets/1.jpg",名称:"4号相机",服务器:2}].map((_ => {让current = new Date();让randomClips = d3.randomUniform(24);让randomTimeSkew = d3.randomUniform(-30,30);让randomType = d3.randomUniform(3);返回相机=>{camera.clips = d3.timeHour.every(Math.ceil(24/randomClips())).range(d3.timeDay.offset(current,-30),d3.timeDay(d3.timeDay.offset(current,1)))).map((d,indexEndTime,g)=> {返回 {startTime:indexEndTime === 0吗?d:d3.timeMinute.offset(d,randomTimeSkew()),endTime:indexEndTime === g.length-1?d3.timeDay(d3.timeDay.offset(current,1)):null,类型:Math.floor(randomType())};}).map((d,indexStartTime,g)=> {if(d.endTime === null)d.endTime = g [indexStartTime +1] .startTime;返回d;});返回相机;};})());让myChart = chart();让selection = d3.select('div#container');selection.datum(data).call(myChart);  

 < div id ="container"></div>< script src ="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>  

下面的缩放处理程序可以正常工作,但是我想要一个更通用的解决方案:

  let newScaleTime = d3.event.transform.rescaleX(scaleTime);d3.select('g#axis').call(axis.scale(newScaleTime));d3.selectAll('rect.clip').attr('width',d => {返回newScaleTime(d.endTime)-newScaleTime(d.startTime);}).attr('x',d => {返回newScaleTime(d.startTime);}); 

解决方案

简短的答案是,您需要实现一个参考比例尺,以指示在未通过缩放操作时该比例尺的基本状态.否则,您将遇到描述的问题:它几乎可以工作,但是重新缩放似乎会累积越来越快的缩放变换."

要了解为什么需要参考比例,请在不移动鼠标的情况下放大图形并缩小(每个图形一次).放大时,轴会改变.缩小时不会.请注意初始放大和第一次缩小时的比例因子:放大时为 1.6471820345351462 ,缩小时为 1 .该数字表示要放大/缩小的大小(无论我们放大多少).在初始放大时,我们放大了〜1.65倍.在前面的缩小中,我们缩小1倍,即:完全不缩小.另一方面,如果您首先进行缩小,则将其缩小约0.6倍,然后如果将其放大,则将其放大1倍.>

 功能chart(){让zoom = d3.zoom().scaleExtent([0.25,20]);让scale = d3.scaleLinear().domain([0,1000]).range([0,550]);让axis = d3.axisBottom;让结果=功能(选择){selection.each(function(){让selection = d3.select(this);selection.call(axis(scale));selection.call(zoom);zoom.on('zoom',function(){标尺= d3.event.transform.rescaleX(scale);console.log(d3.event.transform.k);selection.call(结果);});})}返回结果;}d3.select("svg").call(chart());  

 < script src ="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js></script>< svg width ="550" height ="200"></svg>  

比例应相对于初始缩放因子(通常为1).换句话说,缩放是累积的,它将放大/缩小记录为初始比例的因子,而不是最后一步(否则变换k值)只会是三个值之一:一个用于缩小的值,另一个用于放大的值,一个用于保持相同且相对于当前比例的值.这就是为什么重新缩放初始比例尺不起作用的原因-您将参考点丢失为缩放所参考的初始比例尺.

从文档中,如果您使用 d3.event.transform.rescaleX 重新定义比例,我们将获得一个反映缩放(累积)转换的比例:

[rescaleX]方法不会修改输入比例x;因此代表未转换的比例尺,而返回的比例尺代表其变换后的视图.(文档)

以此为基础,如果我们连续放大两次,则第一次放大时,我们看到transform.k值第一次是〜1.6x,第二次是〜2.7x.但是,由于我们重新缩放了比例,因此我们在已经放大1.6倍的比例上应用了2.7倍的缩放,从而使缩放比例达到了4.5倍而不是2.7倍.更糟糕的是,如果我们放大两次然后缩小一次,则zoom(out)事件会为我们提供一个仍大于1 的缩放值(第一次放大为〜1.6,第二放大为〜2.7,〜1.6),因此尽管滚动了,我们仍在放大:

 功能chart(){让zoom = d3.zoom().scaleExtent([0.25,20]);让scale = d3.scaleLinear().domain([0,1000]).range([0,550]);让axis = d3.axisBottom;让结果=功能(选择){selection.each(function(){让selection = d3.select(this);selection.call(axis(scale));selection.call(zoom);zoom.on('zoom',function(){标尺= d3.event.transform.rescaleX(scale);var放大倍数= 1000/(scale.domain()[1]-scale.domain()[0]);console.log(实际放大倍数:" +放大倍数" x);console.log(预期的放大倍数:" + d3.event.transform.k +"x")console.log("---");selection.call(结果);});})}返回结果;}d3.select("svg").call(chart());  

 < script src ="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js></script>< svg width ="550" height ="200"></svg>  

我没有讨论缩放的x偏移部分,但是您可以想象发生类似的问题-缩放是累积的,但是您丢失了那些累积更改所参考的初始参考点./em>

惯用的解决方案是使用参考比例和缩放比例来创建用于绘制矩形/轴/等的工作比例.最初,工作比例通常与参考比例相同,并设置为:每次缩放时 workingScale = d3.event.transform.rescaleX(referenceScale).

 功能chart(){让zoom = d3.zoom().scaleExtent([0.25,20]);让workingScale = d3.scaleLinear().domain([0,1000]).range([0,550]);让referenceScale = d3.scaleLinear().domain([0,1000]).range([0,550]);让axis = d3.axisBottom;让结果=功能(选择){selection.each(function(){让selection = d3.select(this);selection.call(axis(workingScale));selection.call(zoom);zoom.on('zoom',function(){workingScale = d3.event.transform.rescaleX(referenceScale);var放大倍数= 1000/(workingScale.domain()[1]-workingScale.domain()[0]);console.log(实际放大倍数:" +放大倍数" x);console.log(预期的放大倍数:" + d3.event.transform.k +"x")console.log("---");selection.call(结果);});})}返回结果;}d3.select("svg").call(chart());  

 < script src ="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js></script>< svg width ="550" height ="200"></svg>  

I'm trying to implement semantic zooming while using Mike Bostock's Towards Reusable Charts pattern (where a chart is represented as a function). In my zoom handler, I'd like to use transform.rescaleX to update my scale and then simply call the function again.

It almost works but the rescaling seems to accumulate zoom transforms getting faster and faster. Here's my fiddle:

function chart() {
  let aspectRatio = 10.33;
  let margin = { top: 0, right: 0, bottom: 5, left: 0 };
  let current = new Date();
  let scaleBand = d3.scaleBand().padding(.2);
  let scaleTime = d3.scaleTime().domain([d3.timeDay(current), d3.timeDay.ceil(current)]);
  let axis = d3.axisBottom(scaleTime);
  let daysThisMonth = d3.timeDay.count(d3.timeMonth(current), d3.timeMonth.ceil(current));
  let clipTypes = [ClipType.Scheduled, ClipType.Alarm, ClipType.Motion];
  let zoom = d3.zoom().scaleExtent([1 / daysThisMonth, 1440]);
  let result = function(selection) {
    selection.each(function(data) {
      let selection = d3.select(this);
      let outerWidth = this.getBoundingClientRect().width;
      let outerHeight = outerWidth / aspectRatio;
      let width = outerWidth - margin.left - margin.right;
      let height = outerHeight - margin.top - margin.bottom;
      scaleBand.domain(d3.range(data.length)).range([0, height * .8]);
      scaleTime.range([0, width]);
      zoom.on('zoom', _ => {
        scaleTime = d3.event.transform.rescaleX(scaleTime);
        selection.call(result);
      });
      let svg = selection.selectAll('svg').data([data]);
      let svgEnter = svg.enter().append('svg').attr('viewBox', '0 0 ' + outerWidth + ' ' + outerHeight);//.attr('preserveAspectRatio', 'xMidYMin slice');
      svg = svg.merge(svgEnter);
      	let defsEnter = svgEnter.append('defs');
      	let defs = svg.select('defs');
      	let gMainEnter = svgEnter.append('g').attr('id', 'main');
      	let gMain = svg.select('g#main').attr('transform', 'translate(' + margin.left + ' ' + margin.top + ')');
          let gAxisEnter = gMainEnter.append('g').attr('id', 'axis');
          let gAxis = gMain.select('g#axis').call(axis.scale(scaleTime));
          let gCameraContainerEnter = gMainEnter.append('g').attr('id', 'camera-container');
          let gCameraContainer = gMain.select('g#camera-container').attr('transform', 'translate(' + 0 + ' ' + height * .2 + ')').call(zoom);
      			let gCameraRowsEnter = gCameraContainerEnter.append('g').attr('id', 'camera-rows');
            let gCameraRows = gCameraContainer.select('g#camera-rows');
              let gCameras = gCameraRows.selectAll('g.camera').data(d => {
                return d;
              });
              let gCamerasEnter = gCameras.enter().append('g').attr('class', 'camera');
              gCameras = gCameras.merge(gCamerasEnter);
              gCameras.exit().remove();
                let rectClips = gCameras.selectAll('rect.clip').data(d => {
                  return d.clips.filter(clip => {
                    return clipTypes.indexOf(clip.type) !== -1;
                  });
                });
                let rectClipsEnter = rectClips.enter().append('rect').attr('class', 'clip').attr('height', _ => {
                  return scaleBand.bandwidth();
                }).attr('y', (d, i, g) => {
                  return scaleBand(Array.prototype.indexOf.call(g[i].parentNode.parentNode.childNodes, g[i].parentNode)); //TODO: sloppy
                }).style('fill', d => {
                  switch(d.type) {
                    case ClipType.Scheduled:
                      return '#0F0';
                    case ClipType.Alarm:
                      return '#FF0';
                    case ClipType.Motion:
                      return '#F00';
                  };
                });
                rectClips = rectClips.merge(rectClipsEnter).attr('width', d => {
                  return scaleTime(d.endTime) - scaleTime(d.startTime);
                }).attr('x', d => {
                  return scaleTime(d.startTime);
                });
                rectClips.exit().remove();
      			let rectBehaviorEnter = gCameraContainerEnter.append('rect').attr('id', 'behavior').style('fill', '#000').style('opacity', 0);
          	let rectBehavior = gCameraContainer.select('rect#behavior').attr('width', width).attr('height', height * .8);//.call(zoom);
    });
  };
  return result;
}

// data model

let ClipType = {
  Scheduled: 0,
  Alarm: 1,
  Motion: 2
};
let data = [{
  id: 1,
  src: "assets/1.jpg",
  name: "Camera 1",
  server: 1
}, {
  id: 2,
  src: "assets/2.jpg",
  name: "Camera 2",
  server: 1
}, {
  id: 3,
  src: "assets/1.jpg",
  name: "Camera 3",
  server: 2
}, {
  id: 4,
  src: "assets/1.jpg",
  name: "Camera 4",
  server: 2
}].map((_ => {
  let current = new Date();
  let randomClips = d3.randomUniform(24);
  let randomTimeSkew = d3.randomUniform(-30, 30);
  let randomType = d3.randomUniform(3);
  return camera => {
    camera.clips = d3.timeHour.every(Math.ceil(24 / randomClips())).range(d3.timeDay.offset(current, -30), d3.timeDay(d3.timeDay.offset(current, 1))).map((d, indexEndTime, g) => {
      return {
        startTime: indexEndTime === 0 ? d : d3.timeMinute.offset(d, randomTimeSkew()),
        endTime: indexEndTime === g.length - 1 ? d3.timeDay(d3.timeDay.offset(current, 1)) : null,
        type: Math.floor(randomType())
      };
    }).map((d, indexStartTime, g) => {
      if(d.endTime === null)
        d.endTime = g[indexStartTime + 1].startTime;
      return d;
    });
    return camera;
  };
})());
let myChart = chart();
let selection = d3.select('div#container');
selection.datum(data).call(myChart);

<div id="container"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>

Edit: The zoom handler below works fine, but I'd like a more general solution:

let newScaleTime = d3.event.transform.rescaleX(scaleTime);
d3.select('g#axis').call(axis.scale(newScaleTime));
d3.selectAll('rect.clip').attr('width', d => {
  return newScaleTime(d.endTime) - newScaleTime(d.startTime);
}).attr('x', d => {
  return newScaleTime(d.startTime);
});

解决方案

The short answer is you need to implement a reference scale to indicate what the scale's base state is when unmanipulated by the zoom. Otherwise you will run into the problem you describe: "It almost works but the rescaling seems to accumulate zoom transforms getting faster and faster. "

To see why a reference scale is needed, zoom in on the graph and out (once each) without moving the mouse. When you zoom in, the axis changes. When you zoom out the axis does not. Note the scale factor on the intial zoom in and the first time you zoom out: 1.6471820345351462 on the zoom in, 1 on the zoom out. The number represents how much the to magnify/minify whatever it is we are zooming in on. On the initial zoom in we magnify by a factor of ~1.65. On the preceding zoom out we minify by a factor of 1, ie: not at all. If on the other hand you zoom out first, you minify by a factor of about 0.6 and then if you were to zoom in you magnify by a factor of 1. I've built a stripped down of your example to show this:

function chart() {
  let zoom = d3.zoom().scaleExtent([0.25,20]);
  let scale = d3.scaleLinear().domain([0,1000]).range([0,550]);
  let axis = d3.axisBottom;
  
  let result = function(selection) {
    selection.each(function() {
      
      let selection = d3.select(this);
         
      selection.call(axis(scale));
      selection.call(zoom);
      
      zoom.on('zoom', function() {
         scale = d3.event.transform.rescaleX(scale);
         console.log(d3.event.transform.k);
         selection.call(result);
      });
    
    })
  }
  return result;
}

d3.select("svg").call(chart());

<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
<svg width="550" height="200"></svg>

The scale should be relative to the initial zoom factor, usually 1. In otherwords, the zoom is cumulative, it records magnification/minification as a factor of the initial scale, not the last step (otherwise transform k values would only be one of three values: one value for zooming out, another for zooming in and one for remaining the same and all relative to the current scale). This is why rescaling the initial scale doesn't work - you lose the reference point to the initial scale that the zoom is referencing.

From the docs, if you redefine a scale with d3.event.transform.rescaleX, we get a scale that reflects the zoom's (cumulative) transformation:

[the rescaleX] method does not modify the input scale x; x thus represents the untransformed scale, while the returned scale represents its transformed view. (docs)

Building on this, if we zoom in twice in a row, the first time we zoom in we see the transform.k value is ~1.6x on the first time, the second time it is ~2.7x. But, since we rescale the scale, we apply a zoom of 2.7x on a scale that has already been zoomed in 1.6x, giving us a scale factor of ~4.5x rather than 2.7x. To make matters worse, if we zoom in twice and then out once, the zoom (out) event gives us a scale value that is still greater than 1 (~1.6 on first zoom in, ~2.7 on second, ~1.6 on zoom out), hence we are still zooming in despite scrolling out:

function chart() {
  let zoom = d3.zoom().scaleExtent([0.25,20]);
  let scale = d3.scaleLinear().domain([0,1000]).range([0,550]);
  let axis = d3.axisBottom;
  
  let result = function(selection) {
    selection.each(function() {
      
      let selection = d3.select(this);
         
      selection.call(axis(scale));
      selection.call(zoom);
      
      zoom.on('zoom', function() {
         scale = d3.event.transform.rescaleX(scale);
         var magnification = 1000/(scale.domain()[1] - scale.domain()[0]);
         console.log("Actual magnification: "+magnification+"x");
         console.log("Intended magnification: "+d3.event.transform.k+"x")
         console.log("---");         
         selection.call(result);
      });
    
    })
  }
  return result;
}

d3.select("svg").call(chart());

<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
<svg width="550" height="200"></svg>

I haven't discussed the x offset portion of the zoom, but you can imagine that a similar problem occurs - the zoom is cumulative but you lose the initial reference point that those cumulative changes are in reference to.

The idiomatic solution is to use a reference scale and the zoom to create a working scale used for plotting rectangles/axes/etc. The working scale is initially the same as the reference scale (generally) and is set as so: workingScale = d3.event.transform.rescaleX(referenceScale) on each zoom.

function chart() {
  let zoom = d3.zoom().scaleExtent([0.25,20]);
  let workingScale = d3.scaleLinear().domain([0,1000]).range([0,550]);
  let referenceScale = d3.scaleLinear().domain([0,1000]).range([0,550]);
  let axis = d3.axisBottom;
  
  let result = function(selection) {
    selection.each(function() {
      
      let selection = d3.select(this);
      
      selection.call(axis(workingScale));
      selection.call(zoom);
      
      zoom.on('zoom', function() {
         workingScale = d3.event.transform.rescaleX(referenceScale);
         var magnification = 1000/(workingScale.domain()[1] - workingScale.domain()[0]);
         console.log("Actual magnification: "+magnification+"x");
         console.log("Intended magnification: "+d3.event.transform.k+"x")
         console.log("---");         
         selection.call(result);
      });
    
    })
  }
  return result;
}

d3.select("svg").call(chart());

<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
<svg width="550" height="200"></svg>

这篇关于具有可重用模式的D3语义缩放的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆