2
\$\begingroup\$

I intend to convert strings that are UTC timestamps into a date format, say day + date number (e.g. 'Mon 27'). My current solution is:

  1. Parse the UTC timestamp into a date object using d3.utcParse("%Y-%m-%dT%H:%M:%S")
  2. Convert the date object into a timestamp without hours (i.e. just the date information), using d3.timeFormat("%Y-%m-%d").
  3. Parse the new timestamp again, using d3.timeParse("%Y-%m-%d").

The motivation behind this is that when passing the data into d3.js for plotting purposes, the dates are slightly offset due to (1) the timezone and (2) the daylight saving settings in the locality. This offset causes the dates to be shifted slightly with respect to the axis ticks (see figure 1), and I have therefore used this approach to strip hours, minutes, and seconds information out (see figure 2, with fixed dates).

Dates before fixing, where data points are slightly offset to the right due to daylight saving settings

Figure 1 (above): Dates before fixing, where data points are slightly offset to the right due to daylight saving settings


Fixed dates, with hours, minutes, and seconds stripped out using the procedure stated above

Figure 2 (above): Fixed dates, with hours, minutes, and seconds stripped out using the procedure stated above

This is achieved by chaining the output of three functions, which I found really clunky and chatty, and wondering if there is a better approach to that:

var data = [
  ["2017-03-18T01:00:00", 20],
  ["2017-03-19T01:00:00", 10],
  ["2017-03-20T01:00:00", 5],
  ["2017-03-21T01:00:00", 0],
  ["2017-03-22T01:00:00", 1],
  ["2017-03-23T01:00:00", 12],
  ["2017-03-24T01:00:00", 23],
  ["2017-03-25T01:00:00", 65],
  ["2017-03-26T01:00:00", 78],
  ["2017-03-27T01:00:00", 123]
];

// Functions to parse timestamps
var parseUTCDate = d3.utcParse("%Y-%m-%dT%H:%M:%S");
var formatUTCDate = d3.timeFormat("%Y-%m-%d");
var parseDate = d3.timeParse("%Y-%m-%d");

// Iterate through data
for (let i in data) {
  var timestamp = data[i][0];
  console.log(parseDate(formatUTCDate(parseUTCDate(timestamp))));
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.7.4/d3.min.js"></script>

// Just a normal responce from the server
var response = {
  "status": "Ok",
  "data": [
    ["2017-03-18T01:00:00+00:00", 20],
    ["2017-03-19T01:00:00+00:00", 10],
    ["2017-03-20T01:00:00+00:00", 5],
    ["2017-03-21T01:00:00+00:00", 0],
    ["2017-03-22T01:00:00", 1],
    ["2017-03-23T01:00:00+00:00", 12],
    ["2017-03-24T01:00:00", 23],
    ["2017-03-25T01:00:00+00:00", 65],
    ["2017-03-26T01:00:00+00:00", 78],
    ["2017-03-27T01:00:00+00:00", 123]
  ]
};

// Parse the response
var chart = new Vue({
  el: '#visitors7days',
  data: function() {
    return {
      layout: {
        width: 800,
        height: 400,
        margin: {
          left: 50,
          top: 50,
          right: 50,
          bottom: 50
        }
      },
      plot: {
        points: []
      }
    }
  },

  // Computed functions
  computed: {

    // Return dimensions of SVG chart
    svgViewBox: function() {
      return '0 0 ' + (this.layout.width + this.layout.margin.left + this.layout.margin.right) + ' ' + (this.layout.height + this.layout.margin.top + this.layout.margin.bottom);
    },

    // Stage
    stageTransform: function() {
      return {
        'transform': 'translate(' + this.layout.margin.left + 'px,' + this.layout.margin.top + 'px)'
      }
    }
  },

  // Initialisation
  mounted: function() {

    // Update plot
    this.update();
  },

  // Methods
  methods: {

    // Update elements in chart
    update: function() {
      
      // Internal variables
      var _w = this.layout.width;
      var _h = this.layout.height;
      
      // Date parser
      var parseUTCDate = d3.utcParse("%Y-%m-%dT%H:%M:%S");
      var formatUTCDate = d3.timeFormat("%Y-%m-%d");
      var parseDate = d3.timeParse("%Y-%m-%d");
      var getDate = function(d) {
        return parseDate(formatUTCDate(parseUTCDate(d)));
      };
      
      // Compute scale
      this.plot.scale = {
        x: d3.scaleTime().range([0, _w]),
        y: d3.scaleLinear().range([_h, 0])
      };
      var scale = this.plot.scale;
      
      // Generate area
      this.plot.area = d3.area()
        .x(function(d) { return scale.x(d.date); })
        .y1(function(d) { return scale.y(d.count); });
      
      // Generate line
      this.plot.line = d3.line()
        .x(function(d) { return scale.x(d.date); })
        .y(function(d) { return scale.y(d.count); });
     
      // Push individual points into data
      var _d = response.data;
      for (let i in _d) {
        this.plot.points.push({
          date: getDate(_d[i][0].split('+')[0]),  // Clean up dates with trailing GMT offsets
          count: _d[i][1]
        })
      }
      
      // Set extend of data
      this.plot.scale.x.domain(d3.extent(this.plot.points, function(d) { return d.date; }));
      this.plot.scale.y.domain([0, d3.max(this.plot.points, function(d) { return d.count; })]);
      this.plot.area.y0(this.plot.scale.y(0));
      
      // Draw axes
      d3.select(this.$refs.xAxis)
      .attr('transform', 'translate(0,' + this.layout.height + ')')
      .call(
        d3.axisBottom(scale.x)
       .ticks(7)
        .tickFormat(d3.timeFormat("%a, %b %d"))
      );
      d3.select(this.$refs.yAxis)
      .call(
        d3.axisLeft(scale.y)
      );
      
      // Draw area
      var $area = d3.select(this.$refs.area);
      $area
        .datum(this.plot.points)
        .attr('d', this.plot.area)
        .attr('fill', '#1ABC9C')
        .attr('fill-opacity', 0.5);
      
      // Draw line
      var $line = d3.select(this.$refs.line);
      $line
        .data([this.plot.points])
        .attr('d', this.plot.line);
      
      // Draw points
      var $g = d3.select(this.$refs.points);
      $g.selectAll('circle.point').data(this.plot.points)
        .enter()
        .append('circle')
          .attr('r', 5)
          .attr('class', 'point')
          .attr('cx', function(d) { return scale.x(d.date); })
          .attr('cy', function(d) { return scale.y(d.count); });
      
    }
  }
});
svg {
  background-color: #eee;
  display: block;
  width: 100%;
}
svg g.axis text {
  fill: #555;
}
svg .line {
  fill: none;
  stroke: #159078;
  stroke-width: 2px;
}
svg circle.point {
  fill: #fff;
  stroke: #159078;
  stroke-width: 2px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.5.0/d3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.2.6/vue.min.js"></script>
<div id="visitors7days">
  <svg :view-box.camel="svgViewBox" preserveAspectRatio="xMidYMid meet">
    <g :style="stageTransform">
      <g class="axis x" ref="xAxis"></g>
      <g class="axis y" ref="yAxis"></g>
      <path class="area" ref="area"></path>
      <path class="line" ref="line"></path>
      <g class="points" ref="points"></g>
    </g>
  </svg>
</div>

\$\endgroup\$
3
\$\begingroup\$

I don't understand why you need to parse the date twice. d3.utcParse() will return a Date object. You really don't gain anything from then re-parsing that Date from a different format. You can use that Date object as is to output whatever format you want from it.

If you want to remove the time component from the original date provided, you can use Date.setHours() method to do so. So, if you wanted to map your original data array to an array where the UTC string value have been replaced by Date objects with time values stripped out (i.e. set to 00:00:00.000), that might look like this:

var dataWithDates = data.map(function(el) {
    el[0] = parseUTCDate(el[0]).setHours(0,0,0,0);
    return el;
});

This also might mean you data parser code looks like:

  // Date parser
  var parseUTCDate = d3.utcParse("%Y-%m-%dT%H:%M:%S");
  var getDate = function(d) {
    return parseUTCDate(d).setHours(0,0,0,0);
  };

So you could pick one of these options depending on where you want to make the conversion.

\$\endgroup\$
  • \$\begingroup\$ This worked perfectly fine! I guess I forgot to search for setter methods for the Date object :) the motive of back and forth parsing is to strip the time information out, and I didn't know you can manually set them as such. Thanks again :) \$\endgroup\$ – Terry Mar 28 '17 at 15:02
  • \$\begingroup\$ @Terry Glad to help. It is usually best to think about string/date conversion only at point of input or output, but to actually work with dates and times within Date object context. \$\endgroup\$ – Mike Brant Mar 28 '17 at 15:05

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.