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'))
Related
I need to display elements on my chart with equal interval between elements with dates ticks on X axis. Elements have different interval because they have scaled by date range. Is it possible to display some elements on chart with equal interval between them regardless of dates on X axis?
I have tried to use different scales like bandscale and linearscale. But they works incorrect when i am trying to pass my min and max dates to domain function.
For example this chart:
https://js.devexpress.com/Demos/WidgetsGallery/Demo/Charts/Overview/jQuery/Light/
I need the same equal distance between dots but have dates on my X axis instead of names.
Since it is more important to have the bars equally spaced than to have them spaced according to their time value, use d3.scaleBand() and pass to the domain the full array of dates that are represented in the dataset, e.g., .domain(data.map(d => d.date)).
I'm using d3.js v4.
Is there a more convenient way to find the minimum and maximum values of a brush selection. This is meant to resize the y axis when I select a period in my brush area below.
Here is my method (everything is inside my function called when the brush is used) :
I find the extent limits of my selection
extent = d3.event.selection.map(chartComponent.x2().invert))
Then I have to redo an array containing all my selected points: I go on each point and compare it to the extent[0] or extent[1] to see if it is in the limits. I store the beginning and end point indices and then I use data.slice(begin, end) on my original data to get a new array.
Then apply d3.min and d3.max on the new array to find the min and the max level.
Then set the y axis to use theses limits.
chart.y().domain([chartComponent.ymin, chartComponent.ymax]);
chart.yaxisGraph.call(chartComponent.yAxis(true));
Do someone have a better idea ?
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
In d3, if you want to create an axis you might do something like this:
var xAxis = d3.svg.axis()
.scale(x)
where x is a scale function. I understand that the domain of x defines the start and ending values for the ticks. I'm having trouble understanding how the range of x changes the resulting axis. What does the domain map to in the context of an axis.
Think about what one must do to create a visual representation of any data set. You must convert each data point (e.g. 1 million dollars) into a point on the screen. If your data has a minimum value of $0 and maximum value of $1000000, you have a domain of 0 to 1000000. Now to represent your data on a computer screen you must convert each data point (e.g. $25) into a number of pixels. You could try a simple 1 to 1 linear conversion ($25 converts to 25 pixels on the screen), in which case your range would be the same as your domain = 0 to 1000000. But this would require a bloody big screen. More likely we have an idea of how large we want the graphic to appear on the screen, so we set our range accordingly (e.g. 0 to 600).
The d3 scale function converts each data point in your dataset into a corresponding value within your range. That enables it to be presented on the screen. The previous example is a simple conversion so the d3.scale() function is not doing much for you, but spend some time converting data points into a visual representation and you will quickly discover some situations where the scale function is doing a lot of work for you.
In the particular case of an axis, the scale function is doing exactly the same thing. It is doing the conversion (to pixels) for each 'tick' and placing them on the screen.
I only had 5 values[1,2,3,4,5] as my y - coordinates in the d3.js line plot. But, I end up getting more values [0.5,1,1.5,2,2.5,3,3.5,4,4.5,5] Is there a way to edit the d3.js file or the html file inorder to plot the values as per my requirement?
The tick marks created by a d3 axis can be controlled in two ways:
Using axis.tickValues(arrayOfValues) you can explicitly set the values that you want to show up on the axis. The ticks are positioned by passing each value to the associated scale, so the values should be within your scale's domain. This works for any type of scale, including ordinal scales, so long as the values you give are appropriate to that scale.
Alternately, using axis.ticks(parameters) you can modify the way the scale calculates tick marks. The types of parameters you can use depends on the type of scale you're using -- the values you specify will be passed directly to the scale's .ticks() method, so check the documentation for each scale type. (Parameters will be ignored for ordinal scales, which don't have a ticks() method.)
For linear scales, the scale.ticks() method accepts a number as a parameter; the scale then generates approximately that many ticks, evenly spaced within the domain with round number values. If you do not specify a tick count, the default is to create approximately 10 ticks, which is why you were getting ticks on 0.5 intervals when your domain was from 0 to 5.
So how do you get the behaviour you want (no decimal tick values)?
Using .tickValues(), you would create an array of unique Y-values to be your ticks:
var yValues = data.map(function(d){return d.y;});
//array of all y-values
yValues = d3.set(yValues).values();
//use a d3.set to eliminate duplicate values
yAxis.tickValues( yValues );
Be aware that this approach will use the specified y values even if they aren't evenly spaced. That can be useful (some data visualization books suggest using this approach as an easy way of annotating your graph), but some people may think your graph looks messy or broken.
Using .ticks(), you would figure out the extent of your Y domain, and set the number of ticks so that you do not have more tick marks then you have integers available on your domain:
var yDomain = yScale.domain();
yAxis.ticks( Math.min(10, (yDomain[1] - yDomain[0]) );
This will create the default (approximately 10) ticks for wide domains, but will create one tick per integer value when the difference between the max and min of your domain is less than 10. (Although the tick count is usually approximate, the scale will always prefer integer values if that matches the tick count specified.)
Yes you can also try
yAxis.ticks(5).tickFormat(D3.numberFormat(",d"));
It does the trick of eliminating the decimal numbers, does not effect number of ticks
Here is a good resource for the format of the numbers using D3.