使用可重用模式进行D3语义缩放

时间:2018-05-02 21:30:04

标签: javascript d3.js

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

它几乎可以工作,但重新缩放似乎累积变焦变换越来越快。这是我的小提琴:

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>

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

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);
});

1 个答案:

答案 0 :(得分:1)

简短的回答是,你需要实现一个参考比例,以指示当缩放未进行操作时比例的基本状态。否则你会遇到你描述的问题:“它几乎可以工作,但重新缩放似乎会累积变焦变换变得越来越快。”

要查看为什么需要参考比例,请放大图形并输出(每次一次)而不移动鼠标。放大时,轴会发生变化。当你缩小轴时没有。请注意初始放大和第一次缩小时的缩放系数:缩放时的1.6471820345351462,缩小时的1。这个数字表示放大/缩小我们放大的数量。在初始放大时,我们放大了〜1.65倍。在前面的缩小中,我们缩小1倍,即:根本不缩小。另一方面,如果你首先缩小,你缩小约0.6倍,然后如果你放大你放大1倍。我已经建立了一个剥离你的例子来显示:

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>

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

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

  

[rescaleX]方法不修改输入比例x; x因此   表示未转换的比例,而返回的比例   代表其转变的观点。 (docs

在此基础上,如果我们连续两次放大,我们第一次放大时会看到transform.k值第一次为~1.6x,第二次为~2.7x。但是,由于我们重新调整了比例,我们在已经放大1.6倍的比例上应用了2.7倍的缩放,给出了比例系数为~4.5x而不是2.7x。更糟糕的是,如果我们放大两次然后输出一次,缩放(输出)事件会给我们一个比例值仍然大于1 (第一次放大时为~1.6,第二次放大约为2.7,~1.6)缩小),因此我们仍在放大,尽管向外滚动:

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>

我还没有讨论缩放的x偏移部分,但你可以想象会出现类似的问题 - 缩放是累积的,但是你失去了那些累积变化所引用的初始参考点。 / em>的

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

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>