首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >D3.js v5 -在线性尺度上创建一个相对的、可缩放的类时间轴

D3.js v5 -在线性尺度上创建一个相对的、可缩放的类时间轴
EN

Stack Overflow用户
提问于 2021-04-07 11:30:29
回答 1查看 1.6K关注 0票数 4

我需要在D3 (v5)中创建一个相对的时间线样的轴,它是可缩放的,并在缩放变化时改变单位和刻度间隔。它将用于在相对于基准值0的时间内规划某些活动。

例如时间点:-8天、2小时、10 & 20天、2&4周和6周、4个月等(以毫秒计)。

当放大后,滴答会被格式化为数年,当用户开始放大时,它们会变成几个月,然后是几个星期,几天,几个小时,直到分钟。

CodePen实例

这个例子显示了我所追求的近似效果(使用鼠标轮滚动来放大轴上的大小)。

我决定用一个带整数单位的线性比例--代表毫秒。我使用的是tickFormat(),在这里我找到了滴答之间的距离,并在此基础上计算不同的滴答格式。

我可能不能使用D3的scaleTime(),因为这是基于实际的日历日期(有可变月份、间隔年等)。我需要一个固定规模的补偿-24小时一天,7天周,30天月,365天年。

这个例子中的刻度是错误的-- D3以很好的四舍五入的值自动生成滴答,而且我需要根据缩放级别来改变刻度间隔。这意味着,当以月份的格式显示滴答时,两个滴答之间的距离应该是一个月(以毫秒计),而当放大到小时时,两个滴答之间的距离应该正好是一个小时(以毫秒计),以此类推。

我想我需要创建一些生成变量滴答的算法,但我不确定它是什么样子,以及D3 API的限制是什么,因为我还没有找到任何允许这样的方法。我在任何地方都找不到这种效果的例子,我希望这里的人能为我指明正确的方向,或者给出一些如何实现它的建议。

EN

回答 1

Stack Overflow用户

回答已采纳

发布于 2021-04-10 14:07:38

您可以通过每次缩放后重写滴答标签来使用scaleTime。由于时间尺度是线性尺度的变体,我认为这遇到了一个问题:

时间尺度是具有时间域的线性尺度的变体:

首先,您需要一个原点来映射到0的基线值。在下面的示例中,我选择了2021-03-01,并在几天后任意选择了一个结束日期。

代码语言:javascript
运行
复制
const origin = new Date(2021, 02, 01)
const xScale = d3.scaleTime()
  .domain([origin, new Date(2021, 02, 11)])
  .range([0, width]);

您还需要一个参考标度来确定d3轴生成器决定在该缩放级别上使用的间隔:

代码语言:javascript
运行
复制
const intervalScale = d3.scaleThreshold()
  .domain([0.03, 1, 7, 28, 90, 365, Infinity])
  .range(["minute", "hour", "day", "week", "month", "quarter", "year"]);

这就像说,如果间隔在0到0.03之间,那么就是分钟;在0.03到1之间,是小时;在1到7之间,是几天。我没有投入1/2天,几十年,等等,因为我使用的矩diff函数,它不知道这些间隔。YMMV与其他图书馆。

在轴的初始化和缩放时,调用一个自定义函数,而不是.call(d3.axisBottom(xScale)),因为在这里,您可以根据滴答值和origin之间的相对时间确定标签应该是什么。我叫这个customizedXTicks

代码语言:javascript
运行
复制
const zoom = d3.zoom()
  .on("zoom", () => svg.select(".x-axis")
    .call(customizedXTicks, d3.event.transform.rescaleX(xScale2)))
  .scaleExtent([-Infinity, Infinity]); 

在自定义功能中:

  • t1t2是转换回日期的第2和第3 d3生成的滴答值。使用第二和第三滴答不是什么特别的-只是我的选择。
  • interval在几天内是t2 - t1
  • intervalType使用上面的intervalScale,现在我们可以确定zoom on scaleTime()是否以小时、天、周等为单位。
  • newTicks映射刻度中的刻度值,并使用矩库中的diff函数查找每个滴答值与原点之间的差异,该函数接受来自intervalScale range的值作为参数,以便在缩放级别的正确间隔处得到diff
  • 渲染轴..。
  • 然后用刚刚计算出来的新标签覆盖标签。

覆盖直接访问tick类组--我不相信您可以通过tickValues()将任意文本值传递给scaleTime(),并且很高兴得到纠正:

代码语言:javascript
运行
复制
function customizedXTicks(selection, scale) {
  // get interval d3 has decided on by comparing 2nd and 3rd ticks
  const t1 = new Date(scale.ticks()[1]);
  const t2 = new Date(scale.ticks()[2]);
  // get interval as days
  const interval = (t2 - t1) /  86400000;
  // get interval scale to decide if minutes, days, hours, etc
  const intervalType = intervalScale(interval);
  // get new labels for axis
  newTicks = scale.ticks().map(t => `${diffEx(t, origin, intervalType)} ${intervalType}s`);
  // update axis - d3 will apply tick values based on dates
  selection.call(d3.axisBottom(scale));
  // now override the d3 default tick values with the new labels based on interval type
  d3.selectAll(".x-axis .tick > text").each(function(t, i) {
    d3.select(this)
      .text(newTicks[i])
      .style("text-anchor", "end")
      .attr("dx", "-.8em")
      .attr("dy", ".15em")
      .attr("transform", "rotate(-65)");
  });
  
  function diffEx(from, to, type) {
    let t = moment(from).diff(moment(to), type, true);
    return Number.isInteger(t) ? t : parseFloat(t).toFixed(1);
  }
}

这里这里获取工作示例

代码语言:javascript
运行
复制
const margin = {top: 0, right: 20, bottom: 60, left: 20}
const width = 600 - margin.left - margin.right;
const height = 160 - margin.top - margin.bottom;

// origin (and moment of origin)
const origin = new Date(2021, 02, 01)
const originMoment = moment(origin);

// zoom function 
const zoom = d3.zoom()
  .on("zoom", () => {
    svg.select(".x-axis")
      .call(customizedXTicks, d3.event.transform.rescaleX(xScale2));
    svg.select(".x-axis2")
      .call(d3.axisBottom(d3.event.transform.rescaleX(xScale2)));
  })
  .scaleExtent([-Infinity, Infinity]); 

// x scale
// use arbitrary end point a few days away
const xScale = d3.scaleTime()
  .domain([origin, new Date(2021, 02, 11)])
  .range([0, width]);

// x scale copy for zoom rescaling
const xScale2 = xScale.copy();

// fixed scale for days, months, quarters etc
// domain is in days i.e. 86400000 milliseconds
const intervalScale = d3.scaleThreshold()
  .domain([0.03, 1, 7, 28, 90, 365, Infinity])
  .range(["minute", "hour", "day", "week", "month", "quarter", "year"]);

// svg
const svg = d3.select("#scale")
  .append("svg")
  .attr("width", width + margin.left + margin.right)
  .attr("height", height + margin.top + margin.bottom)
  .call(zoom) 
  .append("g")
  .attr("transform", `translate(${margin.left},${margin.top})`);

// clippath 
svg.append("defs").append("clipPath")
  .attr("id", "clip")
  .append("rect")
  .attr("x", 0)
  .attr("width", width)
  .attr("height", height);
    
// render x-axis
svg.append("g")
  .attr("class", "x-axis")
  .attr("clip-path", "url(#clip)") 
  .attr("transform", `translate(0,${height / 4})`)
  .call(customizedXTicks, xScale); 

// render x-axis
svg.append("g")
  .attr("class", "x-axis2")
  .attr("clip-path", "url(#clip)") 
  .attr("transform", `translate(0,${height - 10})`)
  .call(d3.axisBottom(xScale)); 
  
function customizedXTicks(selection, scale) {
  // get interval d3 has decided on by comparing 2nd and 3rd ticks
  const t1 = new Date(scale.ticks()[1]);
  const t2 = new Date(scale.ticks()[2]);
  // get interval as days
  const interval = (t2 - t1) /  86400000;
  // get interval scale to decide if minutes, days, hours, etc
  const intervalType = intervalScale(interval);
  // get new labels for axis
  newTicks = scale.ticks().map(t => `${diffEx(t, origin, intervalType)} ${intervalType}s`);
  // update axis - d3 will apply tick values based on dates
  selection.call(d3.axisBottom(scale));
  // now override the d3 default tick values with the new labels based on interval type
  d3.selectAll(".x-axis .tick > text").each(function(t, i) {
    d3.select(this)
      .text(newTicks[i])
      .style("text-anchor", "end")
      .attr("dx", "-.8em")
      .attr("dy", ".15em")
      .attr("transform", "rotate(-65)");
  });
  
  function diffEx(from, to, type) {
    let t = moment(from).diff(moment(to), type, true);
    return Number.isInteger(t) ? t : parseFloat(t).toFixed(1);
  }
}
代码语言:javascript
运行
复制
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div id="scale"></div>

在深度缩放时,你可以以一种公平的方式向左和右平移,你会得到相当多的分钟(相对于原点)。如果适合您的用例,您应该能够扩展newTicks逻辑,以获得像10d4h28m这样的标签,而不是14668 minutes

编辑

跟进问题:

放大时,通常有两条标有“0周”、“0节”、“0岁”的蜱。你知不知道这是为什么/如果有可能消除这一点?

我已经在代码片段中包含了第二个轴,它显示了被customizedXTicks函数后处理到相对间隔的原始滴答值。我还将其更改为包含一个内部函数-- diffEx --如果间隔为非整数,则返回一个十进制间隔。

我的理解是,这双0周/季度/年/等的效果是因为D3自动选择间隔。注意下轴线:

  • 当D3决定每两天一次--一周中的某一天可以是一周中的任何一天
  • 当D3决定每7天一次--一周中的一天是星期天时
  • 当D3决定每隔一个月一次,那一周的每一天都是那个月的碰巧。

这意味着,如果你的起源是一个星期二,那么对于,在原点前后的某些区间,它们都将与原点相距小于1间隔,例如,当间隔是这样的时候,每个星期天都被表示为滴答。

在我的第一个回答中,这被表示为00 (可以说是-00)。我把它改成了小数点,以使它更清楚。

HTH

票数 4
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/66985195

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档