简体   繁体   English

在D3中缩小时如何在时间标度中将图标聚类?

[英]How to cluster icons in a time scale upon zooming out in D3?

I have a D3 (using D3 version 3.5.2) time scale chart utilizing only the x-axis at this point. 我现在有一个仅使用x轴的D3(使用D3版本3.5.2)时间表。 The graph plots a series of svg "rect" elements on the x-axis based on an array of dates while it randomizes the data points on the y-axis to prevent clustering issues. 该图基于日期数组在x轴上绘制一系列svg“矩形”元素,同时它在y轴上随机化数据点以防止出现聚类问题。 The chart also has the ability to zoom and pan. 图表还具有缩放和平移的功能。

What I would like to do is upon zooming out of the scale is the following: 我想做的是缩小比例以下:

Cluster all the icons together so that it shows a new icon when zoomed out based on the "collective overlapping amount of events or data points" for a particular vertical slice of the timeline chart as the user zooms out. 将所有图标聚集在一起,以便在用户缩小时根据时间轴图表的特定垂直切片的“事件或数据点的总重叠量”在缩小时显示一个新图标。

For example, if there are seven data points clustered for the month of May, 2018, then it would show an icon with the number of events for that particular vertical time slice (or the month of May, 2018) showing inside the icon, so in this case the number seven will appear in the clustered icon box. 例如,如果在2018年5月的一个月内聚集了七个数据点,则它将显示一个图标,图标中显示该特定垂直时间片(或2018年5月)的事件数,因此在这种情况下,数字7将出现在群集图标框中。 Zooming into May, 2018 would cause the "clustered icon" to disappear and show the actual seven individual "rect" elements displayed across the time scale (so Tue, 23th, Thursday, 25th, etc...). 放大到2018年5月将使“群集图标”消失,并显示整个时间范围内显示的实际七个“矩形”元素(因此,星期二,23,周四,25等)。

The tricky part here would be to grab the actual number of elements for the zoomed out vertical slice of a particular date and render the clustered icon as the user zooms out, and do the opposite when the user zooms in (hide the clustered icon and render individual icons across the timeline). 这里最棘手的部分是获取特定日期的缩小垂直切片的实际元素数量,并在用户缩小时渲染聚类图标,而在用户放大时执行相反的操作(隐藏聚类图标并渲染时间轴上的各个图标)。

Here's my current code: 这是我当前的代码:

 //D3 Timescale demo const width = 1200, height = 500, parsedDate = d3.time.format('%Y-%m-%d').parse; const changedDates = [ '1988-01-01', '1988-01-02', '1988-01-03', '1988-01-04', '1988-01-05', '1988-01-06', '1988-01-07', '1989-01-08', '1989-01-09', '1995-01-10', '1995-01-11', '1998-01-12', '1998-01-13', '1998-01-14', '2002-01-15', '2002-01-16', '2002-01-17', '2004-01-18', '2004-01-19', '2004-01-20', '2004-01-21', '2004-01-22', '2007-01-23', '2007-01-24', '2007-01-25', '2007-01-26', '2007-01-27', '2008-01-28', '2008-01-29', '2008-01-30', '2008-01-31', '2008-02-01', '2010-02-02', '2010-02-03', '2010-02-04', '2012-02-05', '2012-02-06', '2012-02-07', '2012-02-08', '2014-02-09', '2014-02-10', '2014-02-11', '2017-02-12', '2017-02-13', '2017-02-14', '2018-02-15', '2018-02-16', '2018-02-17', '2018-02-18', '2018-02-19', '2018-02-20' ].map(d => parsedDate(d)); const svg = d3.select('#timescale') .append('svg') .attr('preserveAspectRatio', 'xMinYMin meet') .attr('viewBox', `0 0 ${width} ${height}`) .classed('svg-content', true); // .attr('width', width) // .attr('height', height); const clipPath = svg.append('defs') .append('clipPath') .attr('id', 'clip') .append('rect') .attr('width', width - 110) .attr('height', height); const xScale = d3.time.scale() .domain([new Date(Date.parse(d3.min(changedDates, d => d))), new Date(Date.parse(d3.max(changedDates, d => d)))]) .range([10, width - 110]); const yScale = d3.scale.linear() .domain([200, 0]) .range([0, height - 29]); const xAxis = d3.svg.axis() .scale(xScale) .tickSize(1) .orient('bottom'); const yAxis = d3.svg.axis() .scale(yScale) .tickSize(1) .tickValues([0, 100, 200]) .orient('right'); const zoom = d3.behavior.zoom() .on('zoom', function () { svg.select('g.xaxis').call(xAxis).selectAll('text').style('font-size', '10px'); updateEvents(); }).x(xScale); // Draw base area to interact on const rect = svg.append('rect') .attr('x', 0) .attr('y', 0) .attr('width', width - 100) .attr('height', height) .attr('opacity', 0) .call(zoom); svg.append('g') .attr('class', 'xaxis') .attr('transform', 'translate(' + 10 + ',' + 480 + ')') .call(xAxis) .selectAll('text') .style('font-size', '10px'); svg.append('g') .attr('class', 'yaxis') .attr('transform', 'translate(' + 1100 + ',' + 10 + ')') .call(yAxis) .selectAll('text') .style('font-size', '10px'); const renderEvents = dates => { const events = svg.selectAll('rect').data(dates); events.enter() .append('rect') .attr('class', 'item') .attr('x', d => xScale(d)) .attr('y', () => Math.random() * 100) .attr('width', 10) .attr('height', 10) .attr('transform', (d, i) => (i === changedDates.length - 1) ? 'translate(' + 0 + ',' + 362 + ')' : 'translate(' + 10 + ',' + 362 + ')') .attr('clip-path', 'url(#clip)') .style('fill', 'blue'); events.exit() .remove(); } const updateEvents = () => { // The console logs here are to try and figure the distinct amount of inverted x-scale values to try and decipher a pattern for the number of elements // needed to display in the clustered icon box. svg.selectAll('rect.item').attr('x', d => xScale(d)).classed('deleteIcon', d => { console.log('text d: ', Math.floor(xScale(d))); }); console.log(`Elements on chart: ${svg.selectAll('rect.item').size()}`); } renderEvents(changedDates); 
 .svg-container { display: inline-block; position: relative; width: 100%; padding-bottom: 100%; vertical-align: top; overflow: hidden; top: 20px; } .svg-content { display: inline-block; position: absolute; top: 0; left: 0; } 
 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>D3 Timescale Intro</title> <link rel="stylesheet" href="./styles.css"> </head> <body> <div id="timescale" class="svg-container"></div> <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.2/d3.min.js"></script> <script src="./timescale.js"></script> </body> </html> 

I have implemented a D3v5 based on Pan & Zoom Axes 我已经基于平移和缩放轴实现了D3v5

It gives the dates a fixed y-coord during parsing. 它在解析期间为日期提供了固定的y坐标。 It calculates the month groups of the dates. 它计算日期的月份组。 After grouping the key of the group is the Time-in-milliseconds so we have to convert it first to a date when used: new Date().setTime(parseInt(d.key)) 分组后的键是毫秒级时间,因此使用时我们必须先将其转换为日期: new Date().setTime(parseInt(d.key))

Based on the separation of the months on the X-axis it decides to draw the individual points (blue) or the group points (red). 基于X轴上月份的间隔,它决定绘制单个点(蓝色)或组点(红色)。

You have to tweak the styling of the text and its alignment. 您必须调整文本的样式及其对齐方式。 And the group points are not clipped for some reason. 并且由于某些原因,组点没有被裁剪。

Every zoom/pan action all is redrawn based on a new xScale . 所有缩放/平移动作均基于新的xScale重新绘制。

<!DOCTYPE html>
<meta charset="utf-8">
<style>

.axis path {
  display: none;
}

.axis line {
  stroke-opacity: 0.3;
  shape-rendering: crispEdges;
}

.view {
  fill: none;
  stroke:none;
}

button {
  position: absolute;
  top: 20px;
  left: 20px;
}

</style>
<button>Reset</button>
<div id="timescale" class="svg-container"></div>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script>

//D3 Timescale demo

const width = 1200,
    height = 500,
    parsedDate = d3.utcParse("%Y-%m-%d");

const changedDates = [
    '1988-01-01', '1988-01-02', '1988-01-03',
    '1988-01-04', '1988-01-05', '1988-01-06',
    '1988-01-07', '1989-01-08', '1989-01-09',
    '1995-01-10', '1995-01-11', '1998-01-12',
    '1998-01-13', '1998-01-14', '2002-01-15',
    '2002-01-16', '2002-01-17', '2004-01-18',
    '2004-01-19', '2004-01-20', '2004-01-21',
    '2004-01-22', '2007-01-23', '2007-01-24',
    '2007-01-25', '2007-01-26', '2007-01-27',
    '2008-01-28', '2008-01-29', '2008-01-30',
    '2008-01-31', '2008-02-01', '2010-02-02',
    '2010-02-03', '2010-02-04', '2012-02-05',
    '2012-02-06', '2012-02-07', '2012-02-08',
    '2014-02-09', '2014-02-10', '2014-02-11',
    '2017-02-12', '2017-02-13', '2017-02-14',
    '2018-02-15', '2018-02-16', '2018-02-17',
    '2018-02-18', '2018-02-19', '2018-02-20'
].map(d => { return { date: parsedDate(d), y: Math.random() * 100 + 50 }; });

const svg = d3.select('#timescale')
        .append('svg')
            .attr('preserveAspectRatio', 'xMinYMin meet')
            .attr('viewBox', `0 0 ${width} ${height}`)
            .classed('svg-content', true);
        // .attr('width', width)
        // .attr('height', height);

const clipPath = svg.append('defs')
    .append('clipPath')
    .attr('id', 'clip')
    .append('rect')
    .attr('width', width - 110)
    .attr('height', height);

var minDate = d3.min(changedDates, d => d.date);
var maxDate = d3.max(changedDates, d => d.date);
minDate.setUTCFullYear(minDate.getUTCFullYear()-1);
maxDate.setUTCFullYear(maxDate.getUTCFullYear()+1);

const xScale = d3.scaleTime()
    .domain([minDate, maxDate])
    .range([10, width - 110]);

const yScale = d3.scaleLinear()
    .domain([200, 0])
    .range([0, height - 29]);

const xAxis = d3.axisBottom()
    .scale(xScale)
    .tickSize(1);

const yAxis = d3.axisRight()
    .scale(yScale)
    .tickSize(1)
    .tickValues([0, 100, 200]);

var view = svg.append("rect")
    .attr("class", "view")
    .attr("x", 0.5)
    .attr("y", 0.5)
    .attr("width", width - 109)
    .attr("height", height - 28);

var gX = svg.append('g')
    .attr('class', 'xaxis')
    .attr('transform', `translate(10,${height-20})`)
    .call(xAxis);
    // .selectAll('text')
    // .style('font-size', '10px');

svg.append('g')
    .attr('class', 'yaxis')
    .attr('transform', 'translate(' + 1100 + ',' + 10 + ')')
    .call(yAxis);
    // .selectAll('text')
    // .style('font-size', '10px');

var zoom = d3.zoom()
    .scaleExtent([1, 100])
//    .translateExtent([[-100, -100], [width + 90, height + 100]])
    .on("zoom", zoomed);

d3.select("button")
    .on("click", resetted);

svg.call(zoom);

var points = svg.append("g")
    .attr('class', 'points');

var monthCount = d3.nest()
    .key(function(d) { return Date.UTC(d.date.getUTCFullYear(), d.date.getUTCMonth(), 1); })
    .rollup(function(v) { return { count: v.length, y: Math.random() * 100 + 50 }; })
    .entries(changedDates);

function drawDates(dates, xScale) {
  var points = svg.select(".points");
  points.selectAll(".item").remove();
  // use domain to group the dates
  var minDate = xScale.domain()[0];
  var minDatep1m = new Date(minDate.getTime()+30*24*60*60*1000); // + 1 month
  var deltaX = xScale(minDatep1m) - xScale(minDate);
  if (deltaX > 20) {
    points.selectAll('.item')
      .data(dates)
      .enter()
      .append('rect')
      .attr('class', 'item')
      .attr('x', d => xScale(d.date))
      .attr('y', d => d.y)
      .attr('width', 10)
      .attr('height', 10)
      .attr('clip-path', 'url(#clip)')
      .style('fill', 'blue');
  } else {
    var groups = points.selectAll('.item')
      .data(monthCount)
      .enter()
      .append('g')
      .attr('class', 'item')
      .attr('transform', d => `translate(${xScale(new Date().setTime(parseInt(d.key)))},${d.value.y})`);
    groups.append('rect')
      .attr('x', -5)
      .attr('y', -5)
      .attr('width', 10)
      .attr('height', 10)
      .attr('clip-path', 'url(#clip)')
      .style('fill', 'red');
    groups.append('text')
      .text(d => d.value.count);
  }
}

drawDates(changedDates, xScale);

function zoomed() {
  var transformedX = d3.event.transform.rescaleX(xScale);
  gX.call(xAxis.scale(transformedX));
  drawDates(changedDates, transformedX)
}

function resetted() {
  svg.transition()
      .duration(750)
      .call(zoom.transform, d3.zoomIdentity);
}

</script>

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM