D3 Non-Continuous Dates Domain Gives Gaps on X-Axis - javascript

I want to plot some time series data that is not continuous (gaps in the dates for weekends, holidays, etc..). It's daily data.
The data looks something like:
date,value
1/2/15,109.33
1/5/15,106.25
1/6/15,106.26
1/7/15,107.75
1/8/15,111.89
1/9/15,112.01
1/12/15,109.25
1/13/15,110.22
...
So I define my x and y scales:
var x = d3.time.scale().range([0, width]);
var y = d3.scale.linear().range([height, 0]);
And set the domain from my source data:
x.domain(d3.extent(data, function(d) { return d.date; }));
y.domain(d3.extent(data, function(d) { return d.value; }));
The problem however, is that my dates have gaps in them. And my x-axis now includes those missing dates (d3.time.scale() does this automatically I guess because it maps to a continuous range?).
.extent() finds the max and min values in date then .domain() returns those max and min values as the range for the x-axis. But im not sure how to handle gaps and return the non gap dates to the range.
So my question is: How can I have my x-axis range only include the dates that are in my data set? And not "fill in the blanks". Should I play with the d3.time.scale().range() ? or do I need to use a different scale?
What is the correct way to do this? Can someone please point me in the right direction or give some example? Thank you!
Also please, I want to solve with with just plain d3 and javascript. I am not interested in using any 3rd party abstraction libraries.

As Lars Kotthof points out, you can create an ordinal x axis, which "looks" like a time scale.
Assuming you want to plot your time series as a line, here is an example how to do it: http://jsfiddle.net/ee2todev/3Lu46oqg/
If you want to customize your format for the dates, e.g. respresent the date as day (of the week), day and month you have to convert your string to a date first.
I added one example which formats the dates in a common German format. But you can easily adjust the code to your needs.
http://jsfiddle.net/ee2todev/phLbk0hf/
Last, but not least, you can use
axis.tickValues([values])
to choose the dates you want to display. See also:
https://github.com/mbostock/d3/wiki/SVG-Axes#tickFormat

The d3fc-discontinuous-scale component adapts any other scale (for example a d3 time scale) and adding the concept of discontinuities.
These discontinuities are determined via a 'discontinuity provider' the built in discontinuitySkipWeekends allows you to skip weekends.
Here's an example:
const skipWeekendScale = fc.scaleDiscontinuous(d3.scaleTime())
.discontinuityProvider(fc.discontinuitySkipWeekends());
And here's a complete demo: https://bl.ocks.org/ColinEberhardt/0a5cc7ca6c256dcd6ef1e2e7ffa468d4

Related

How to sort x Axis based on dates with anychart Library?

I have a problem with multiples series data. I have three series data (sometimes two), where the x values are dates and the y values are numbers.
I'm not able to sort the column bar based on date.
I reproduced the issue here: https://playground.anychart.com/WuqV384h
As you can see, the series data are showed as are written inside the "series" key. The dates should be sorted from 2019 to 2020.
A similar issue was asked here.
However, even using the DateTime, I'm not able to visualize the dates in the right order. If I use DateTime, the chart doesn't show the real value of the date (es. x: 2020-02-02), and the chart doesn't fit the container.
Thank you in advance.
The Cartesian Column chart uses an Ordinal scale. It doesn't provide any sorting because the Ordinal scale works with categories (names), it doesn't differ dates or names.
So, there are two available solutions:
Use real dateTime xScale that sorts points automatically. But it is a linear scale. Here is the sample.
Use the ordinal scale, but in this case, you need to preprocess your data to get the sorted array of categories and apply it to the xScale. Here is the sample.

D3 calculate x position from dates

I have a D3 plot where I plot time (hh:mm) on x against some values on y axis.
For the x scale I use this logic
const currentData : string[] = ['06:00', '12:00', '18:00'];
this.x = d3Scale.scalePoint(
[0, this.width - this.margin.right - this.margin.left])
.domain(
currentData.map(
(d: PlotData) => d.time))
.round(true);
That plots all my data nicely. Now I would like to use this.x to return the value of a time point that is currently not in my original data. But when I run it with
this.x('14:00')
it returns NAN , which seems is because the input is not in the currentData array and to work only on values from the array.
Do I need to interpolate this value myself or is there a D3 function to take this.x and figure the this.x('14:00') internally?
Thanks,
EL
The other answer suggests the right approach, to use d3-scaleTime. I'd like to dig into why you see your problem, and then provide details to the solution with d3-scaleTime.
Ordinal Scales
d3.scalePoint is an ordinal scale. The domain represents a discrete and discontinuous set of values. The mapping of the domain to the range is based on the order in which domain values are specified. The first value in the domain is mapped to the first value in the range (with d3.scalePoint) regardless of its value as compared to other values in the domain.
As a result, only values that exist in the domain can be mapped to the range: there is no semantic/quantitative relationship between values in the domain that allows interpolation of in between values. Because you are using quantitative rather than qualitative or ordinal data, you want to use a scale that treats the domain as continuous.
There may be cases where you'd want to use an d3.scalePoint/Band scale with time based data, but in those cases you'd need to be able to construct a complete domain of all values that need to be plotted.
That your scale appears to plot your data as continuous at first is coincidental, if your middle domain value was '07:00', 'June' or 'yesterday', it would appear in the middle of your range. That you chose '12:00' hides the fact that the scale isn't positioning this based on its value but only on its index.
Continuous Scales
d3.scaleTime is one of d3's continuous scales
Continuous scales map a continuous, quantitative input domain to a continuous output range. (docs)
Domains and Ranges
For d3's continuous scales, domain and range must contain the same number of elements - if there are more than two elements in each, the domain and range are split into segments:
.domain([a,b,c]) // where a,b,c are quantitative
.range([1,2,3])
Values between a and b will be interpolated between 1 and 2, likewise, values between b and c will be interpolated between 2 and 3. If domain and range have a different number of elements, D3 will skip excess values of the one that has more.
It appears in your case that just two domain values will be sufficient, the minimum and maximum values you want to pass to the scale.
Using Dates
d3.scaleTime's domain needs to be set with date objects not a string such as '12:00', also, when passing a value to the scale (scale(x)) we need to pass a date object. We can create a basic d3 date parser with:
d3.parseTime("%H:%M")
Which gives us:
const currentData = ['06:00', '12:00', '18:00'];
const parse = d3.timeParse("%H:%M")
const x = d3.scaleTime()
.range([0,100])
.domain(d3.extent(currentData, (time) => parse(time)))
console.log("15:00 scales to: ", x(parse('15:00')))
console.log("06:00 scales to: ", x(parse('06:00')))
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
This should map your domain as before, but also allows you to also plot values that aren't in your initial dataset and have them mapped properly to the range based on their value.
It might be easier to instead use scaleTime
const currentData : string[] = ['06:00', '12:00', '18:00'];
this.x = d3.scaleTime()
.domain((d: PlotData) => d.time))
.range([0, this.width - this.margin.right - this.margin.left]);
this.x(new DateTime('xyz:abc'))

custom ticks on an ordinal scale d3

I've been creating some charts to plot intra-day data from financial results. Often this is a value generated every ten minutes. This varies but its a good example. Therefore there are large periods of time I won't get information, eg when markets are close over weekend and between 5:00pm in the evening and 9:00pm in the morning. I've tried created a custom time scale for the x-axis but in the end the best result is to just use an ordinal scale. it works well and gives the result that I and the people reading the chart want ie, no gaps in the line plot and evenly space data points. (Its the convention)
My question is how do I know plot custom ticks on this xAxis in the correct place, given that I generate them in an array called ticks.major. The example below shows how I generate the axis and there are the correct number of days. But they all are being plotted at the beginning of the graph. Any help appreciated thanks.
var xScale = d3.scale.ordinal()
//var xScale = scaleWeekday()...custom timescale no longer used
.domain(xDomain)
.rangeBands([0,(plotWidth-yLabelOffset)])
var xAxis = d3.svg.axis()
.scale(xScale)
//.tickValues(ticks.major)//used to create tick d3 time scale axis
.tickFormat(function (d,i) {
return ticks.major[i]
})
.tickSize(yOffset/2)
.orient("bottom");
var xLabel=plot.append("g")
.attr("class",media+"xAxis")
.attr("transform",function(){
if(yAlign=="right") {
return "translate("+(margin.left)+","+(plotHeight+margin.top)+")"
}
else {return "translate("+(margin.left+yLabelOffset)+","+(plotHeight+margin.top)+")"}
})
.call(xAxis);
it looks like this:
I think the mistake I'm making is that by using the tick.major array its applying the tick value to the first 12 dates that are passed bacause thats all thats in the tick.major array. Then because there are no more dates in the tick.majore array it has no labels for any further date in the series. Its better to apply a test in the tickFormetter to see if the current day at a particular datapoint is different from the day at the previous datapoint. Then return a tick. like this
.tickFormat(function (d,i) {
if(i>0) {
var day=d.getDay()
var yesterday=data[i-1].date.getDay()
console.log(day,yesterday)
if(day!=yesterday) {
return d
}
}
})
The returned d needs a bit of date formatting to make it readable

NVD3 Line chart tics wrongly aligned to grid when using datetype on xAxis

I am currently working with NVD3 using Angular Directive (angular-nvd3). I have a very simple line chart with very simple data.
The problem I have encountered now is that my data is wrongly aligned with the Axis. Example plunker available here: http://plnkr.co/edit/jWEYt6?p=preview ,
I am using dates on my xAxis, which are parsed using d3 library:
tickFormat: function(d) {return d3.time.format('%d/%m')(new Date(d))}
Description:
I would expect the xAxis labels to be correspondent to the grid.
In the example you can clearly notice that the xAxis is not evenly devided (values: 06/11, 08/11, 11/11, 13/11). So usually 2 days and sometimes 3 days :)
What is worse - the peaks are not matching the grid. Example: 06/11 tick is really not even close to the grid's line where I guess it is supposed to be.
I have also tried this on master's code from repo and it happens there too. There is a link in the HTML head section.
Is there a problem with my data, proper date formatting or something else? Thanks!
This bugged me for a while and I could not find an answer here. I even have opened a bug on GitHub: https://github.com/novus/nvd3/issues/1382#issuecomment-160694559 and I was clued in on the answer.
The problem:
The actual issue is hidden because of d3.time.format('%d/%m'). My example data is given in one tick per day manner, and the format was set accordingly. But d3 does not understand that. When drawing the grid it divides the max-min/someValue and the grid ticks does not have to occur on full day (midnight), but on any hour. And because of the formatting I could not see that.
The version showing this misconception is here: http://plnkr.co/edit/2iMHOp?p=preview
Solution:
So now, when I know what I could do, I managed to substitute the ticks by using tickValues parameter in nvd3 / angular wrapper.
The version with the solution is here:
http://plnkr.co/edit/23n3ll?p=preview
Yet another bug :)
Funny thing is that since the labels are too long to be displayed, I had to rotate them so they could fit. Another bug occurs here (I think). As you can see 2nd and last but one tick label is missing. First I tried using the solution mentioned here: NVD3 Line Chart X Axis Ticks Are Missing using the showMaxMin parameter but it does not work correctly. But if you rotate the labels to ~ -70 degrees the labels are displayed OK.
I guess this is not the end with my NVD3 journey ;)
Since the problem is, according to Atais:
The actual issue is hidden because of d3.time.format('%d/%m'). My example data is given in one tick per day manner, and the format was set accordingly. But d3 does not understand that. When drawing the grid it divides the max-min/someValue and the grid ticks does not have to occur on full day (midnight), but on any hour. And because of the formatting I could not see that.
I managed to pass the x's values as integer values (ex: 20160211) instead of formatted dates (ex: 2016-02-11 or similars) to nvd3, and then on tickFormatformat them to display properly.
I wrote another plunker with the problem and the commented solution (used momentjs):
Plunker with the simulated error: http://plnkr.co/edit/fXDQ0f?p=preview
Data is provided in format x: milliseconds, y: int, like {x: 1446418800000, y: 20}, and it is being formated with tickFormat:
xAxis: {
tickFormat: function(d) {
return moment(d).format('YYYY-MM-DD');
}
}
Plunker with the solution: http://plnkr.co/edit/KpALzo?p=preview
Data is provided in format x: int, y: int, like {x: 20160211, y: 20}, and it is being formated with tickFormat:
xAxis: {
tickFormat: function(d) {
moment(d, 'YYYYMMDD').format('YYYY-MM-DD');
}
}
Note that you can do it with time too, just by appending to the 'numeric date'.
As stated from #ajaybc, will not work well with dates from different months, since d3 will interpolate X axis with invalid filling dates (days 32, 33, 34 and so on)

D3.extent issues with month value confusion over first and last day of month

I have created a Plunkr for my issue, found at the link
I am having a problem with scaling when using month values. I am sure this is a simple fix.
A visualization of my issue is below (look at December):
In short, when using d3.extent in an update pattern for the axis and the bars, d3 is not computing the domain correctly. It seems to think it is less than it is and so I have one entry too many, leading one bar to hang off and the x axis to be one value too long.
My xScale is computed normally:
xScale = d3.time.scale()
.domain(d3.extent(data,function(d){return d.funded_month}))
.rangeRound([0, (w + offsetBar) - (marginleft + marginright)]);
Both the x Axis, bars and text are all working off the same domain.
When visualizing all years, the viz appears correctly:
Is the issue that the number of values in each respective visualization are different (13 years vs 12 months? What is the mistake?
I was not able to implement an easy solution with existing libraries.
Because D3 interprets the "Month, Year" format assuming the day is the first of the month at midnight, January to December is considered only 11 months.
So I stepped the days of every month to create a smooth axis:
2001-01-01,68991364
2001-02-04,554541108
2001-03-07,151123291
2001-04-10,283000000
2001-05-12,8093737
2001-06-15,55718802
2001-07-18,95060100
2001-08-21,78000000
2001-09-23,150000000
2001-10-25,193330000
2001-11-28,59948530
2001-12-31,142424927
I was able to use an x translate on my axis text to move in sync with the bars.
Mike Bostock goes further into this issue here.

Categories