且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

D3.js时间刻度:当数据以秒为单位时,以分钟的间隔进行精确间隔的刻度?

更新时间:2023-10-27 17:10:46

通常在制作时间范围时,您可以使用 d3。 time.scale() ,而不是线性刻度。



您的情况有点奇怪,因为您使用的是抽象持续时间,而不是特定时间。不幸的是,似乎d3内置的时间功能不是很适合这种情况。有几个选项我可以想到的解决方法:






选项1:使用线性刻度手动 .tickValues()



而不是使用 Date object。您可以简单地将您的数据值(以秒为单位)分解为小时,分钟和秒。类似这样:

  formatMinutes = function(d){
var hours = Math.floor(d / 3600) ,
minutes = d - (分钟* 60);
分钟= Math.floor((d-(小时* 3600)
var output = seconds +'s';
if(minutes){
output = minutes +'m'+ output;
}
if(小时){
输出=小时+'h'+输出;
}
return output;
};

基本上,这需要总秒数,每3600秒创建一个小时,创建一分钟对于每个剩余的60秒,并且最后返回剩余的秒。然后输出一个字符串表示,例如: 17s 12m 42s 4h 8m 22s $ c>。



然后当你做轴时,可以使用 .tickValues()方法将零范围指定为数据的最大值,步长为600,因为在10分钟内有600秒。这看起来像这样:

  var x = d3.scale.linear()
.domain([ d3.max(values)])
.range([0,width]);

var xAxis = d3.svg.axis()
.scale(x)
.orient(bottom)
.tickFormat(formatMinutes)
.tickValues(d3.range(0,d3.max(values),600));

以下是 JSFiddle 输出。






选项2:固定持续时间为 .ticks()



时间刻度可让您直接指定每10分钟。你只需要向你的轴的 .ticks()方法传递一个d3持续时间和一个乘数即可。像这样:

  var xAxis = d3.svg.axis()
.scale(x)
.orient(bottom)
.ticks(d3.time.minute,10)

为此,您必须首先设置您的时间刻度。对于您的范围的域,可以使用一个毫秒值范围,因为d3将它们转换为 Date 对象。在这种情况下,由于您的数据以秒为单位,因此我们可以简单地乘以1000来获取毫秒。在这种情况下,我们将最大值舍入到最接近的毫秒,因为它必须是一个整数,以使有效的日期:

  var x = d3.time.scale()
.domain([0,Math.ceil(d3.max(values)* 1000)])
.range([0,width]) ;

最后,您可以使用将您的格式直接传递到轴。 tickFormat()

  var xAxis = d3.svg.axis()
。 scale(x)
.orient(bottom)
.ticks(d3.time.minute,10)
.tickFormat(d3time.format('%Mm%Ss') );

然而,在这一点上,我需要指出一些东西,因为正如我提到的,时间函数不适合处理抽象持续时间。我将更改 .tickFormat 以显示小时数:

  .tickFormat(d3.time.format('%Hh%Mm%Ss')); 

请查看 JSFiddle 的结果是什么...



根据你在世界上的位置,你会得到一个不同的值的小时位置。我在美国东海岸,所以我的营业时间为 19 。这是从哪里来的?不应该是零吗?



不幸的是,当我们使标度的域从0到最大数据值的毫秒数时,它创建常规日期对象,使用毫秒输入的那些值。这意味着它们表示自1970年1月1日午夜UTC时间以来的毫秒数。这里在美国的东部时区,这意味着它是1969年12月31日19:00:00。这是19来自哪里,或任何其他值你得到。



如果您知道您的所有数据将少于1小时,那么您可以忽略这一点。如果你需要使用小时的地方,你可以通过强制d3使用UTC时间格式化轴使用 d3.time.format.utc()

  .tickFormat(d3.time.format.utc('%Hh%Mm%Ss'))

这里是 JSFiddle 更新为使用UTC。



现在,您可以看到小时数为0。



当然,如果您的任何数据超过24小时,此方法将无法工作,您必须手动执行轴选项1 。 / p>




希望这有助于至少让你开始,但这是一个棘手的问题,一个内置在库中的优雅解决方案来处理这个问题。也许它会使d3的git repo一个良好的功能请求。我想听听@mbostock是否有任何建议,如何处理d3中的抽象持续时间,而不必绑定到日期对象,这需要引用绝对点


I'm working with a D3 time scale. My input data is in seconds, and it's duration data rather than dates - so 10 seconds, 30 seconds etc.

I want to create an axis that lets me do the following:

  • Display ticks formatted in minutes and seconds: like "0m 30s", "1m 00s", etc. This formatting on its own is fairly straightforward, but not when I also need to...
  • Display ticks at intervals that look neat when formatted in minutes. If I just use D3's default tick formatting then I get ticks at intervals that make sense in minutes, but not seconds.

Here is my code:

var values = [100,200,300....]; // values in seconds
var formatCount = d3.format(",.0f"),
    formatTime = d3.time.format("%Mm %Ss"),
    formatMinutes = function(d) { 
        var t = new Date(2012, 0, 1, 0, 0, d);
        t.setSeconds(t.getSeconds() + d);
        return formatTime(t); 
    };
var x = d3.scale.linear()
    .domain([0, d3.max(values)])
    .range([0, width]);
var xAxis = d3.svg.axis()
    .scale(x)
    .orient("bottom")
    .tickFormat(formatMinutes);

This gives me nicely-formatted ticks at irregular intervals: "16m 40s", "33m 20s" etc. How can I generate ticks at "10m 00s", "20m 00s", etc?

The obvious answer would be to transform the values array into minutes, use a linear scale and write a formatter to handle it, but I'd prefer to use a time scale if possible

Here is a JSFiddle to demonstrate the problem: http://jsfiddle.net/83Xmf/

Normally when making a time scale, you would use d3.time.scale(), rather than a linear scale.

Your case is a little odd in that you are using abstract durations of time, and not specific points in time for your data. Unfortunately it seems that d3's built in time functionality is not well-suited to this case. There are a couple of options I can think of for workarounds:


Option 1: Use a linear scale with manual .tickValues()

Rather than formatting your ticks using a Date object. You could simply break down your data value (which is in seconds) into hours, minutes, and seconds. Something like this:

formatMinutes = function(d) { 
    var hours = Math.floor(d / 3600),
        minutes = Math.floor((d - (hours * 3600)) / 60),
        seconds = d - (minutes * 60);
    var output = seconds + 's';
    if (minutes) {
        output = minutes + 'm ' + output;
    }
    if (hours) {
        output = hours + 'h ' + output;
    }
    return output;
};

Basically, this takes the total number of seconds, creates an hour for every 3600 seconds, creates a minute for each remaining 60 seconds, and finally gives back the remaining seconds. Then it outputs a string representation, for example: 17s or 12m 42s or 4h 8m 22s.

Then when you make your axis, you can use the .tickValues() method to assign a range from zero to your data's max value, going by steps of 600, since there are 600 seconds in 10 minutes. That would look like this:

var x = d3.scale.linear()
  .domain([0, d3.max(values)])
  .range([0, width]);

var xAxis = d3.svg.axis()
  .scale(x)
  .orient("bottom")
  .tickFormat(formatMinutes)
  .tickValues(d3.range(0, d3.max(values), 600));

Here's a JSFiddle of the output.


Option 2: Use a time scale with a fixed duration for .ticks()

Time scales let you specify directly that you'd like ticks every 10 minutes. You do that simply by passing a d3 duration and a multiplier to the .ticks() method of your axis. Like this:

var xAxis = d3.svg.axis()
  .scale(x)
  .orient("bottom")
  .ticks(d3.time.minute, 10)

In order to do this, you must first set up your time scale. For the domain of your scale, you can use a range of millisecond values, since d3 will turn these into Date objects. In this case, since your data is in seconds, we can simply multiply by 1000 to get milliseconds. In this case we'll round up the max value to the nearest millisecond, since it must be an integer to make a valid date:

var x = d3.time.scale()
  .domain([0, Math.ceil(d3.max(values) * 1000)])
  .range([0, width]);

Finally, you can pass your format in directly to the axis, using .tickFormat():

var xAxis = d3.svg.axis()
  .scale(x)
  .orient("bottom")
  .ticks(d3.time.minute, 10)
  .tickFormat(d3.time.format('%Mm %Ss'));

However, at this point I need to point something out because, as I mentioned, the built-in time functions are not well-suited to dealing with abstract durations. I'm going to change the .tickFormat to show the hours as well:

.tickFormat(d3.time.format('%Hh %Mm %Ss'));

Have a look at the JSFiddle of what the result would be...

Depending on where you are in the world, you'll get a different value for the hours place. I'm on the East coast of the US, so my hours place says 19. Where is that coming from? Shouldn't it be zero?

Well, unfortunately, when we made the domain of the scale go from 0 to the number of milliseconds of the largest data value, it created regular Date objects, using those values for the millisecond input. This means that they represent the number of milliseconds since midnight UTC time on January 1, 1970. Here in the Eastern time zone of the US, that means it was 19:00:00 on December 31, 1969. That's where the 19 comes from, or whatever other value you get.

If you know that all of your data will be less than 1 hour, then perhaps you can just ignore this. If you need to use an hours place, you can work around this by forcing d3 to use UTC time to format the axis using d3.time.format.utc():

.tickFormat(d3.time.format.utc('%Hh %Mm %Ss'))

Here's the JSFiddle updated to use UTC.

Now you can see that the hour is 0 as expected.

Of course, if any of your data is ever longer than 24 hours, this method won't work at all, and you'll have to resort to doing the axis manually as in Option 1.


Hopefully this helps to at least get you started, it's a tricky problem though, and there doesn't seem to be an elegant solution built into the library for handling this. Perhaps it would make for a good feature request on d3's git repo. I'd love to hear if @mbostock has any suggestions on how to handle abstract durations of time in d3 without having to be tied to Date objects, which require references to absolute points in time.