3
\$\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>

200_success
146k22 gold badges190 silver badges479 bronze badges
asked Mar 28, 2017 at 11:53
\$\endgroup\$

1 Answer 1

4
\$\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.

answered Mar 28, 2017 at 14:51
\$\endgroup\$
2
  • \$\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\$ Commented Mar 28, 2017 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\$ Commented Mar 28, 2017 at 15:05

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.