gpt4 book ai didi

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

转载 作者:行者123 更新时间:2023-12-04 03:30:24 27 4
gpt4 key购买 nike

我需要在 D3 (v5) 中创建一个类似时间轴的相对轴,该轴可缩放并在缩放更改时更改单位和刻度间隔。
它将用于相对于基线 - 值 0 及时规划某些事件。
示例时间点:在 -8 天、2 小时、10 和 20 天、2 和 4 和 6 周、4 个月等(以毫秒偏移量存储)。
当缩小时,刻度会被格式化为年,当用户开始放大时,这些会变成月,然后是周、天、小时,再到分钟。
Axis example screenshot
CodePen Example
该示例显示了我想要的近似效果(使用鼠标滚轮滚动放大或缩小轴)。
我决定使用整数单位的线性标度 - 代表毫秒。
我正在使用 tickFormat()我在这里找到刻度之间的距离并基于此计算不同的刻度格式。
我可能无法使用 D3 的 scaleTime()因为这是基于真实的日历日期(具有可变月份、间隔年等)。我需要固定的偏移量 - 24 小时天、7 天周、30 天月、365 天年。
示例中的比例是错误的 - D3 自动以四舍五入的值自动生成刻度 - 我需要刻度间隔根据缩放级别是可变的。意思是,当以月格式显示刻度时,2 个刻度之间的距离应该是一个月(以毫秒为单位),当缩小到小时时,2 个刻度之间的距离应该正好是一小时(以毫秒为单位)等等。
我想我需要创建一些生成变量刻度的算法,但我不确定它会是什么样子,以及 D3 API 的限制是什么,因为我还没有找到任何允许这样的方法。
我在任何地方都找不到这种效果的任何例子,我希望这里有人能指出我正确的方向或提供一些关于如何实现它的提示。

最佳答案

您可以使用 scaleTime通过在每次缩放后覆盖刻度标签。自 time scales是线性尺度的变体,我觉得这符合问题:

Time scales are a variant of linear scales that have a temporaldomain:


首先,您需要一个原点来映射到基线值 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]);
在自定义函数中:
  • t1t2是第二个和第三个 d3 生成的刻度值转换回日期。使用第 2 个和第 3 个刻度并不特别 - 只是我的选择。
  • intervalt2 - t1在天
  • intervalType使用 intervalScale上面,我们现在可以确定 zoomscaleTime()在例如小时、天、周等
  • newTicks映射刻度中的刻度值,并使用 diff 找出每个刻度值与原点之间的差异。矩库中的函数接受来自 intervalScale 的值range作为参数,你得到 diff以正确的缩放级别间隔
  • 渲染轴...
  • 然后用您刚刚计算的新标签覆盖标签

  • 覆盖访问 tick直接分类组 - 我不相信您可以将任意文本值传递给 scaleTime()来自 tickValues()并且很高兴得到纠正:
    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);
    }
    }
    工作示例来自 herehere :

    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如果这适合您的用例。
    编辑
    后续问题:

    when zoomed, there are often two ticks marked '0 weeks', '0 quarters','0 years' as well. Any idea why is that / if it is possible toeliminate that ?


    我在片段中包含了第二个轴,显示了由 customizedXTicks 后处理到相对间隔的原始刻度值。功能。我还更改了它以包含一个内部函数 - diffEx - 如果间隔是非整数,则返回小数间隔。
    我的理解是双 0 周/季度/年/等的这种影响是因为 D3 自动选择了间隔。注意下轴:
  • 当 D3 决定间隔 2 天时 - 一周中的哪一天可以是一周中的任何一天
  • 当 D3 决定 7 天间隔时 - 一周中的某一天是星期日
  • 当 D3 决定月份间隔时 - 一周中的哪一天是那个月碰巧是什么

  • 所以这意味着如果您的来源是例如星期二,然后是 一些 原点之前和之后的间隔,它们都离原点小于 1 个间隔,例如其中间隔是这样的,每个星期天都被渲染为刻度线。
    在我的第一个答案中,这呈现为两者,例如 00 (并且可以说是 -00 )。我已将其更改为小数以使其更清楚。
    HTH

    关于javascript - D3.js v5 - 在线性尺度上创建一个相对的、可缩放的类似时间轴的轴,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/66985195/

    27 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com