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
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>