D3 - 图表刷过时重置缩放?

时间:2014-11-29 17:23:31

标签: javascript svg d3.js

我正在制作D3时标/价格表金融图表。图表SVG本身使用zoom()来几何平移和缩放数据并重新绘制轴。图表下方是一个SVG画笔窗格,它以高级别显示整个数据集,并允许自行平移。我面临的问题与此小提琴中显示的行为相同(不是我的代码):http://jsfiddle.net/p29qC/8/。刷牙后缩放会导致跳跃行为,因为zoom()从未从brush()中获取更改。

缩放和刷子可以独立工作,但我无法让它们协同工作。当图表被刷过时,我希望缩放能够检测到这一点,所以下次图表缩放时,它会在画笔停止的地方拾取。反之亦然。

我已设法设置一个同步功能,以便在启动缩放时让画笔正确更新,但我无法反向工作 - 在导航器中进行画笔时更新图表缩放。我找了好几个小时无济于事。有什么补丁可以解决这个问题吗?我为长代码块道歉,但我希望设置上下文有帮助!

设置代码(为简洁起见省略了一些基本变量):

// Create svg
var svg = d3.select('#chart')
  .append('svg')
  .attr({
    class: 'fcChartArea',
    width: width+margin.left+margin.right,
    height: height+margin.bottom,
  })
  .style({'margin-top': margin.top});

// Create group for the chart
var chart = svg.append('g');

// Clipping path
chart.append('defs').append('clipPath')
  .attr('id', 'plotAreaClip')
  .append('rect')
  .attr({
    width: width,
    height: height
  });

// Create plot area, using the clipping path
var plotArea = chart.append('g')
  .attr({
    class: 'plotArea',
    'clip-path': 'url(#plotAreaClip)'
  });

// Compute mins and maxes
var minX = d3.min(data, function (d) {
  return new Date(d.startTime*1000);
});
var maxX = d3.max(data, function (d) {
  return new Date(d.startTime*1000);
});
var minY = d3.min(data, function (d) {
  return d.low;
});
var maxY = d3.max(data, function (d) {
  return d.high;
});

// Compute scales & axes
var dateScale = d3.time.scale()
  .domain([minX, maxX])
  .range([0, width]);
var dateAxis = d3.svg.axis()
  .scale(dateScale)
  .orient('bottom');
var priceScale = d3.scale.linear()
  .domain([minY, maxY])
  .nice()
  .range([height, 0]);
var priceAxis = d3.svg.axis()
  .scale(priceScale)
  .orient('right');

// Store initial scales
var initialXScale = dateScale.copy();
var initialYScale = priceScale.copy();

// Add axes to the chart
chart.append('g')
  .attr('class', 'axis date')
  .attr('transform', 'translate(0,' + height + ')')
  .call(dateAxis);
chart.append('g')
  .attr('class', 'axis price')
  .attr('transform', 'translate(' + width + ',0)')
  .call(priceAxis);

// Compute and append the OHLC series
var series = fc.series.ohlc('path')
  .xScale(dateScale)
  .yScale(priceScale);
var dataSeries = plotArea.append('g')
  .attr('class', 'series')
  .datum(data)
  .call(series);

// Create the SVG navigator
var navChart = d3.select('#chart')
  .classed('chart', true)
  .append('svg')
    .classed('navigator', true)
    .attr('width', navWidth + margin.left + margin.right)
    .attr('height', navHeight+margin.top+margin.bottom)
    .style({'margin-bottom': margin.bottom})
    .append('g');
// Compute scales & axes
var navXScale = d3.time.scale()
  .domain([minX, maxX])
  .range([0, navWidth]);
var navXAxis = d3.svg.axis()
  .scale(navXScale)
  .orient('bottom');
var navYScale = d3.scale.linear()
  .domain([minY, maxY])
  .range([navHeight, 0]);
// Add x-axis to the chart
navChart.append('g')
  .attr('class', 'axis date')
  .attr('transform', 'translate(0,' + navHeight + ')')
  .call(navXAxis);
// Add data to the navigator
var navData = d3.svg.area()
  .x(function (d) {
    return navXScale(new Date(d.startTime*1000));
  })
  .y0(navHeight)
  .y1(function (d) {
    return navYScale(d.close);
  });
var navLine = d3.svg.line()
  .x(function (d) {
    return navXScale(new Date(d.startTime*1000));
  })
  .y(function (d) {
    return navYScale(d.close);
  });
navChart.append('path')
  .attr('class', 'data')
  .attr('d', navData(data));
navChart.append('path')
  .attr('class', 'line')
  .attr('d', navLine(data));

// create brush viewport
var viewport = d3.svg.brush()
  .x(navXScale)
  .on("brush", brush);

// add brush viewport to the SVG navigator
navChart.append("g")
  .attr("class", "viewport")
  .call(viewport)
  .selectAll("rect")
  .attr("height", navHeight);

// set zoom behavior
var zoom = d3.behavior.zoom()
  .x(dateScale)
  .scaleExtent([1, 12.99])
  .on('zoom', zoom);

// Create zoom pane
plotArea.append('rect')
  .attr('class', 'zoom-overlay')
  .attr('width', width)
  .attr('height', height)
  .call(zoom);

画笔和缩放功能:

// zoom - brush synchronizations
function updateBrushFromZoom() {
  if ((dateScale.domain()[0] <= minX) && (dateScale.domain()[1] >= maxX)) {
    viewport.clear();
  } else {
    viewport.extent(dateScale.domain());
  }
  navChart.select('.viewport').call(viewport);
}

function updateZoomFromBrush() {
  // help!!
}

function brush() {
  var g = d3.selectAll('svg').select('g');
  var newDomain = viewport.extent();
  if (newDomain[0].getTime() !== newDomain[1].getTime()) {
    dateScale.domain([newDomain[0], newDomain[1]]);
    var xTransform = fc.utilities.xScaleTransform(initialXScale, dateScale);

    // define new data set
    var range = moment().range(newDomain[0], newDomain[1]);
    var rangeData = [];
    for (var i = 0; i < data.length; i += 1) {
      if (range.contains(new Date(data[i].startTime*1000))) {
        rangeData.push(data[i]);
      }
    }
    // define new mins and maxes
    var newMinY = d3.min(rangeData, function (d) {
      return d.low;
    });
    var newMaxY = d3.max(rangeData, function (d) {
      return d.high;
    });

    // set new yScale
    priceScale.domain([newMinY, newMaxY]);
    var yTransform = fc.utilities.yScaleTransform(initialYScale, priceScale);

    // draw new axes on main chart
    g.select('.fcChartArea .date.axis')
      .call(dateAxis);
    g.select('.fcChartArea .price.axis')
      .call(priceAxis);

    // transform the data to fit new chart viewport
    g.select('.series')
      .attr('transform', 'translate(' + xTransform.translate + ',' + yTransform.translate+ ')' + ' scale(' + xTransform.scale + ',' + yTransform.scale + ')');
  }
  else {
    // remove transformation
    g.select('.series')
      .attr('transform', null);
  }
  updateZoomFromBrush();
}

// Zoom functions
function zoom() {
  var g = d3.selectAll('svg').select('g');
  // set new xScale
  var newDomain = dateScale.domain();
  var xTransformTranslate = d3.event.translate[0];
  var xTransformScale = d3.event.scale;

  // define new data set
  var range = moment().range(newDomain[0], newDomain[1]);
  var rangeData = [];
  for (var i = 0; i < data.length; i += 1) {
    if (range.contains(new Date(data[i].startTime*1000))) {
      rangeData.push(data[i]);
    }
  }

  // define new max and min
  var newMinY = d3.min(rangeData, function (d) {
    return d.low;
  });
  var newMaxY = d3.max(rangeData, function (d) {
    return d.high;
  });

  // set new yScale
  priceScale.domain([newMinY, newMaxY]);
  var yTransform = fc.utilities.yScaleTransform(initialYScale, priceScale);

  // draw new axes on main chart
  g.select('.fcChartArea .date.axis')
    .call(dateAxis);
  g.select('.fcChartArea .price.axis')
    .call(priceAxis);

  // transform the data to fit new chart viewport
  g.select('.series')
    .attr('transform', 'translate(' + xTransformTranslate + ',' + yTransform.translate+ ')' + ' scale(' + xTransformScale + ',' + yTransform.scale + ')');

  // update SVG navigator
  updateBrushFromZoom();
}

助手功能:

fc.utilities.yScaleTransform = function(oldScale, newScale) {
  var oldDomain = oldScale.domain();
  var newDomain = newScale.domain();
  var scale = (oldDomain[1] - oldDomain[0]) / (newDomain[1] - newDomain[0]);
  var translate = scale * (oldScale.range()[1] - oldScale(newDomain[1]));
  return {
    translate: translate,
    scale: scale
  };
};

fc.utilities.xScaleTransform = function(oldScale, newScale) {
  var oldDomain = oldScale.domain();
  var newDomain = newScale.domain();
  var scale = (oldDomain[1] - oldDomain[0]) / (newDomain[1] - newDomain[0]);
  var translate = scale * (oldScale.range()[0] - oldScale(newDomain[0]));
  return {
    translate: translate,
    scale: scale
  };
};

1 个答案:

答案 0 :(得分:3)

updateZoomFromBrush()中,使用zoom.x(dateScale)将缩放重新绑定到缩放行为。

这是必需的,因为d3.behavior.zoom()对您传入的比例的副本进行操作,因此如果不重新调整比例,行为将不会对比例域进行任何更改在brush()

请参阅此示例http://bl.ocks.org/mbostock/3892928