我需要在D3 (v5)中创建一个相对的时间线样的轴,它是可缩放的,并在缩放变化时改变单位和刻度间隔。它将用于在相对于基准值0的时间内规划某些活动。
例如时间点:-8天、2小时、10 & 20天、2&4周和6周、4个月等(以毫秒计)。
当放大后,滴答会被格式化为数年,当用户开始放大时,它们会变成几个月,然后是几个星期,几天,几个小时,直到分钟。

这个例子显示了我所追求的近似效果(使用鼠标轮滚动来放大轴上的大小)。
我决定用一个带整数单位的线性比例--代表毫秒。我使用的是tickFormat(),在这里我找到了滴答之间的距离,并在此基础上计算不同的滴答格式。
我可能不能使用D3的scaleTime(),因为这是基于实际的日历日期(有可变月份、间隔年等)。我需要一个固定规模的补偿-24小时一天,7天周,30天月,365天年。
这个例子中的刻度是错误的-- D3以很好的四舍五入的值自动生成滴答,而且我需要根据缩放级别来改变刻度间隔。这意味着,当以月份的格式显示滴答时,两个滴答之间的距离应该是一个月(以毫秒计),而当放大到小时时,两个滴答之间的距离应该正好是一个小时(以毫秒计),以此类推。
我想我需要创建一些生成变量滴答的算法,但我不确定它是什么样子,以及D3 API的限制是什么,因为我还没有找到任何允许这样的方法。我在任何地方都找不到这种效果的例子,我希望这里的人能为我指明正确的方向,或者给出一些如何实现它的建议。
发布于 2021-04-10 14:07:38
您可以通过每次缩放后重写滴答标签来使用scaleTime。由于时间尺度是线性尺度的变体,我认为这遇到了一个问题:
时间尺度是具有时间域的线性尺度的变体:
首先,您需要一个原点来映射到0的基线值。在下面的示例中,我选择了2021-03-01,并在几天后任意选择了一个结束日期。
const origin = new Date(2021, 02, 01)
const xScale = d3.scaleTime()
.domain([origin, new Date(2021, 02, 11)])
.range([0, width]);您还需要一个参考标度来确定d3轴生成器决定在该缩放级别上使用的间隔:
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
const zoom = d3.zoom()
.on("zoom", () => svg.select(".x-axis")
.call(customizedXTicks, d3.event.transform.rescaleX(xScale2)))
.scaleExtent([-Infinity, Infinity]); 在自定义功能中:
t1和t2是转换回日期的第2和第3 d3生成的滴答值。使用第二和第三滴答不是什么特别的-只是我的选择。interval在几天内是t2 - t1intervalType使用上面的intervalScale,现在我们可以确定zoom on scaleTime()是否以小时、天、周等为单位。newTicks映射刻度中的刻度值,并使用矩库中的diff函数查找每个滴答值与原点之间的差异,该函数接受来自intervalScale range的值作为参数,以便在缩放级别的正确间隔处得到diff。覆盖直接访问tick类组--我不相信您可以通过tickValues()将任意文本值传递给scaleTime(),并且很高兴得到纠正:
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);
}
}
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);
}
}<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自动选择间隔。注意下轴线:
这意味着,如果你的起源是一个星期二,那么对于,在原点前后的某些区间,它们都将与原点相距小于1间隔,例如,当间隔是这样的时候,每个星期天都被表示为滴答。
在我的第一个回答中,这被表示为0和0 (可以说是-0和0)。我把它改成了小数点,以使它更清楚。
HTH
https://stackoverflow.com/questions/66985195
复制相似问题