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:
- Parse the UTC timestamp into a date object using
d3.utcParse("%Y-%m-%dT%H:%M:%S")
- Convert the date object into a timestamp without hours (i.e. just the date information), using
d3.timeFormat("%Y-%m-%d")
. - 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).
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>
1 Answer 1
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.
-
\$\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– Terry2017年03月28日 15:02:49 +00:00Commented 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\$Mike Brant– Mike Brant2017年03月28日 15:05:19 +00:00Commented Mar 28, 2017 at 15:05