在 D3js 中的 x 轴上使用自定义刻度标签

Using custom tick lables on x-axis in D3js

我在 D3js 中制作了一个热图,x 轴为一年中的某天(1..365 之间的整数),y 轴为一天中的时间(1..24 之间的整数)。对于一年中的每个小时,一个点通过其颜色指示温度。

我试图找到一种方法,通过使用序数比例将月份的名称映射到一年中的正确日期,但没有成功。相反,我创建了一个带有月份名称的新 xScale 变量。问题是数据不再显示了——现在我只看到 x 轴和 y 轴。

我想这是有道理的,因为 x 轴基于日期,并且每个点的 x 坐标基于整数 (1..365)。

所以我的问题是:

  1. 如何将年 (d.day) 转换为日期,是否有效?
  2. 如果我将 d.day 转换为日期,这将如何影响这样的计算:var days = d3.max(dataset, function(d) { return d.day; }) - d3.min(dataset, function(d) { return d.day; });
  3. 这个问题有没有更好的解决办法?

下面是我的全部代码(抱歉我不知道在哪里设置限制):

d3.csv("data-gitignore/temperatures_nyc.csv", function(error, dataset) {
    dataset.forEach(function(d) {
        d.tOutC = +d.tOutC;
        d.day = +d.day;
        d.hour = +d.hour;
    });

    //----------------------------------
    // VARIABLES
    //----------------------------------

    var days = d3.max(dataset, function(d) { return d.day; })
        - d3.min(dataset, function(d) { return d.day; });
    var hours = d3.max(dataset, function(d) { return d.hour; })
        - d3.min(dataset, function(d) { return d.hour; });

    var tMin = d3.min(dataset, function(d) { return d.tOutC; }),
        tMax = d3.max(dataset, function(d) { return d.tOutC; });

    var dotWidth = 1,
        dotHeight = 3,
        dotSpacing = 0.5;

    var margin = {top: 0, right: 25, bottom: 40, left: 25},
        width = (dotWidth * 2 + dotSpacing) * days,
        height = (dotHeight * 2 + dotSpacing) * hours;//200 - margin.top - margin.bottom;

    //----------------------------------
    // FUNCTIONS
    //----------------------------------

    var xScale = d3.time.scale()
        .domain([new Date(2015, 0, 1), new Date(2015, 11, 31)])
        .range([0, width]);

    var yScale = d3.scale.linear()
        .domain([1, hours])
        .range([(dotHeight * 2 + dotSpacing) * hours, dotHeight * 2 + dotSpacing]);

    var colorScale = d3.scale.linear()
        .domain([tMin-5, (tMax-tMin)/2, tMax-5])
        .range(["#4575b4","#ffffdf", "#d73027"]);

    var xAxis = d3.svg.axis()
        .scale(xScale)
        .orient("bottom")
        .ticks(d3.time.months)
        .tickFormat(d3.time.format("%b"));


    // Define Y axis
    var yAxis = d3.svg.axis()
        .scale(yScale)
        .orient("left")
        .ticks(2);

    // Zoom behavior
    var zoom = d3.behavior.zoom()
        .scaleExtent([dotWidth, dotHeight])
        .x(xScale)
        .on("zoom", zoomHandler);

    function zoomHandler() {
        var t = zoom.translate(),
            tx = t[0],
            ty = t[1];

        tx = Math.min(tx, 0);
        tx = Math.max(tx,  width - (dotWidth * 2 + dotSpacing) * days * d3.event.scale);
        zoom.translate([tx, ty]);


        svg.select(".x.axis").call(xAxis);
        svg.selectAll("ellipse")
            .attr("cx", function(d) { return xScale(d.day); })
            .attr("cy", function(d) { return yScale(d.hour); })
            .attr("rx", function(d) { return (dotWidth * d3.event.scale); });
    }

    // SVG canvas
    var svg = d3.select("#chart")
        .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 + ")");

    // Clip path
    svg.append("clipPath")
        .attr("id", "clip")
        .append("rect")
        .attr("width", width)
        .attr("height", height);


    //----------------------------------
    // DRAW ELEMENTS ON SVG CANVAS
    //----------------------------------


    // Heatmap dots
    svg.append("g")
        .attr("clip-path", "url(#clip)")
        .selectAll("ellipse")
        .data(dataset)
        .enter()
        .append("ellipse")
        .attr("cx", function(d) { return xScale(d.day); })
        .attr("cy", function(d) { return yScale(d.hour); })
        .attr("rx", dotWidth)
        .attr("ry", dotHeight)
        .attr("fill", function(d) { return colorScale(d.tOutC); });

    //Create X axis
    svg.append("g")
        .attr("class", "x axis")
        .attr("transform", "translate(0," + yScale(0) + ")")
        .call(xAxis)

    //Create Y axis
    svg.append("g")
        .attr("class", "y axis")
        .call(yAxis);

});

首先,将一年的第一天设置为参考日期,date0。如果年份是 2014 年,那么它看起来像这样:

var date0 = Date.UTC(2014, 0);// Midnight on January 1st, 2014

date0是自1970年以来的毫秒数。要验证这一点,你可以说

console.log(new Date(date0).toUTCString);

//> "Wed, 01 Jan 2014 00:00:00 GMT"

(注意这里使用的是 UTC。最好在任何地方都使用 UTC 日期,因为这样可以确保用户无论在哪个时区都能看到正确的日期)。

鉴于此,您可以像这样轻松地为一年中的任何时间+日期创建日期对象:

// constants
var msPerHour = 3600000,
    msPerDay  = 24 * 3600000;

var after3days8hours = new Date(date0 + 3 * msPerDay + 8 * msPerHour);
console.log(after3days8hours.toUTCString())

//> "Sat, 04 Jan 2014 08:00:00 GMT"

现在您可以计算数据集中每个 CSV 行的日期:

dataset.forEach(function(d) {
    d.tOutC = +d.tOutC;
    d.day = +d.day;
    d.hour = +d.hour;
    d.date = new Date(date0 + d.day * msPerDay + d.hour * msPerDay)
});

这回答了问题 #1。

关于问题 #2:完成所有这些将使您能够根据需要计算 min/max:

d3.max(dataset, function(d) { return d.date; })

d3.min(dataset, function(d) { return d.date; });

即使 d.date 是一个 Date 对象,JavaScript 也应该为您将其转换回毫秒数。此外,即使您选择跳过所有这些 Date 对象的实例化,以上所有内容都可以正常工作。如:

// without new Date
d.date = date0 + d.day * msPerDay + d.hour * msPerDay;

但是 d3.time.scale 需要传递日期而不是数字,因此例如 xScale(d.date) 需要是 xScale(new Date(d.date)) 如果 d.date 是一个数字。如果您曾经看到 d3 出现控制台错误,请怀疑您可能忘记了这一点并输入了数字而不是日期。

对于 cx 计算,对于任何给定的时间,您都需要找到当天的午夜。您可以像这样使用 d.day

.attr("cx", function(d) {
  return xScale(new Date(date0 + d.day * msPerDay));
})

或者,您可以像这样从 d.date 算出:

.attr("cx", function(d) {
  var msAtMidnight = Math.floor(d.date / msPerDay) * msPerDay
  return xScale(new Date(msAtMidnight));
})

要将 d.date 用于 cy,您可以说:

var msSinceMidnight = d.date % msPerDay

最后,因为您将使用 UTC 日期,所以您需要 UTC 时间标度而不是现有的常规时间标度。 D3 有适合你的体重秤:

var xScale = d3.time.scale.utc()

我认为 yScale 域应该是 [0, 23]

关于问题 #3:这可能是最好的解决方案,特别是因为 x 轴的刻度为月份(一月、二月等)可能是最合适的。为了获得这些滴答声,您有点必须使用时间尺度,因为每个月的天数是可变的,并且在没有 "time-aware" 尺度的情况下进行计算需要大量工作。