I'm constructing a graph right now which is taking in data from a postgres backend. For the construction of the x-axis, I have the following:
var x = d3.scaleTime()
.domain(d3.extent(data_prices, function(d){
var time = timeParser(d.timestamp);
return time;
}))
.range([0,width])
where timeParser is a function representing d3.timeParse().
I have a data point which is at 16:58 and another at 22:06 and it looks a little ugly having it just stick at the side like that. How would I say, for instance, have there be a slight padding of say, +/- 30 minutes for each and continue the trendline path on each end? (or at least just the first part)
To create a padding in a time scale, use interval.offset. According to the API:
Returns a new date equal to date plus step intervals. If step is not specified it defaults to 1. If step is negative, then the returned date will be before the specified date.
Let's see it working. This is an axis based on a time scale with a min and a max similar to yours:
var svg = d3.select("svg");
var data = ["16:58", "18:00", "20:00", "22:00", "22:06"].map(function(d) {
return d3.timeParse("%H:%M")(d)
});
var scale = d3.scaleTime()
.domain(d3.extent(data))
.range([20, 580]);
var axis = d3.axisBottom(scale);
var gX = svg.append("g")
.attr("transform", "translate(0,50)")
.call(axis)
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="600" height="100"></svg>
Now, to create the padding, we just need to subtract and add 30 minutes at the extremes:
var scale = d3.scaleTime()
.domain([d3.timeMinute.offset(d3.min(data), -30),
//subtract 30 minutes here --------------^
d3.timeMinute.offset(d3.max(data), 30)
//add 30 minutes here ------------------^
])
.range([20, 580]);
Here is the result:
var svg = d3.select("svg");
var data = ["16:58", "18:00", "20:00", "22:00", "22:06"].map(function(d) {
return d3.timeParse("%H:%M")(d)
});
var scale = d3.scaleTime()
.domain([d3.timeMinute.offset(d3.min(data), -30), d3.timeMinute.offset(d3.max(data), 30)])
.range([20, 580]);
var axis = d3.axisBottom(scale);
var gX = svg.append("g")
.attr("transform", "translate(0,50)")
.call(axis)
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="600" height="100"></svg>
Have in mind that this solution does not round the extreme ticks to the nearest half hour: it adds and subtracts exactly half an hour.
So, to round to the nearest half hour, you can do a simple math using Math.floor and Math.ceil:
.domain([
d3.min(data).setMinutes(Math.floor(d3.min(data).getMinutes() / 30) * 30),
d3.max(data).setMinutes(Math.ceil(d3.max(data).getMinutes() / 30) * 30)
])
Here is the demo:
var svg = d3.select("svg");
var data = ["16:58", "18:00", "20:00", "22:00", "22:06"].map(function(d) {
return d3.timeParse("%H:%M")(d)
});
var scale = d3.scaleTime()
.domain([d3.min(data).setMinutes(Math.floor(d3.min(data).getMinutes() / 30) * 30), d3.max(data).setMinutes(Math.ceil(d3.max(data).getMinutes() / 30) * 30)])
.range([20, 580]);
var axis = d3.axisBottom(scale);
var gX = svg.append("g")
.attr("transform", "translate(0,50)")
.call(axis)
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="600" height="100"></svg>
Related
I'm trying to work out how to change the formatting for months when drawing an axis with a scaleTime data range.
I've been able to manage the layout as expected, but i'd like the full month names to be truncated. See the example below - I'd like "February" to be "Feb".
The other dates are fine.
The summary of my axis code is:
const x = scaleTime()
.domain(dateExtent)
.rangeRound([marginSize.left, width - marginSize.right]);
const xAxis = axisBottom(x)
.ticks(timeDay.every(2))
.tickSizeOuter(0);
Is there a means of providing the formatting for those date values?
The easiest and fastest alternative is just using tickFormat with the specifier you want, so all ticks will have the same structure.
However, assuming you want to change only the boundary ticks (for a format that doesn't match the adjacent ones, as you described), you can get the ticks after the axis generator created them and check their value.
For instance, an axis with February, like yours:
const svg = d3.select("svg");
const scale = d3.scaleTime()
.range([20, 580])
.domain([new Date("January 20, 2020"), new Date("February 10, 2020")]);
const axis = d3.axisBottom(scale)(svg.append("g").attr("transform", "translate(0,50)"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="500" height="100"></svg>
We get the ticks and check if the text matches the desired specifier, otherwise we change it:
d3.selectAll(".tick text").each(function(d) {
if (this.textContent !== d3.timeFormat("%a %d")(d)) this.textContent = d3.timeFormat("%b")(d);
});
Here is the result:
const svg = d3.select("svg");
const scale = d3.scaleTime()
.range([20, 580])
.domain([new Date("January 20, 2020"), new Date("February 10, 2020")]);
const axis = d3.axisBottom(scale)(svg.append("g").attr("transform", "translate(0,50)"));
d3.selectAll(".tick text").each(function(d) {
if (this.textContent !== d3.timeFormat("%a %d")(d)) this.textContent = d3.timeFormat("%b")(d);
});
svg.append("ellipse")
.attr("cx", 342)
.attr("cy", 63)
.attr("rx", 20)
.attr("ry", 8)
ellipse {
fill: none;
stroke: red;
stroke-width: 2;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="500" height="100"></svg>
I have this code: https://plnkr.co/edit/t1GKaQOGnFxkTMK03Ynk
var MIN = 60;
var HR = MIN * 60;
var tripDomain = [0, 15*MIN, 30*MIN, 45*MIN, 60*MIN, 90*MIN, Math.max(24*HR, d3.max(data))];
var bins = d3.histogram()
.thresholds(tripDomain)(data);
var x = d3.scaleOrdinal()
.domain(tripDomain)
.range(tripDomain.map((_,i) => (i/(tripDomain.length-1))*width));
// ...more code...
chart.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
That renders a histogram correctly. The only issue is that the axis first tick is painting 0 and 61 on top of each other. It should be only 0... not sure where the 61 is coming from (it's the first value of the data) but I don't see how it ended up in the axis.
Notice the first value on the axis... Could you shed some light on this issue?
...not sure where the 61 is coming from.
It's coming from the scale's domain, which you are changing when you append the rectangles and the texts! And that's the expected behaviour, since you're using an ordinal scale.
Explanation:
Your x scale domain is well defined:
[0, 900, 1800, 2700, 3600, 5400, 1280703]
However, in your first bin, you have this (as array properties):
[x0: 61, x1: 900]
So, whenever you use the x0 of the first bin with the x scale, for instance:
.attr("transform", function(d){
return "translate(" + x(d.x0) + "," + y(d.length) + ")";
});
...you're actually introducing a new value in the domain (for a detailed explanation, see my answer here), since there is no 61 in the x scale domain.
By the way, this new value is introduced at the end of the domain and, despite being very close to 0, that 61 is actually the last tick.
Solution:
Declare the unknown:
var x = d3.scaleOrdinal()
.unknown(0)
Here is your updated plunker: https://plnkr.co/edit/n1IoEkHcL0sGUraDVCfc?p=preview
Here are the dates that I am parsing:
2010-12-31
2011-12-31
2012-12-31
2013-12-31
2014-12-31
2015-12-31
2016-12-31
Here is my code:
this.x = d3.scaleTime().domain(d3.extent(this.dataArray, d => {return d[ this.xType ];})).range([ this.margin.left, this.width - this.margin.right ]);
this.y0 = d3.scaleLinear().domain([ this.getMin(this.yType1, this.yType0), this.getMax(this.yType1, this.yType0) ]).range([ this.height, 0 ]);
this.y1 = d3.scaleLinear().domain([ this.getMin(this.yType1, this.yType0), this.getMax(this.yType1, this.yType0) ]).range([ this.height, 0 ]);
this.xAxis = d3.axisBottom(this.x);
this.yAxisLeft = d3.axisLeft(this.y0).ticks(5);
this.yAxisRight = d3.axisRight(this.y1).ticks(5);
The problem is that the first date (2010) is being truncated from the x-axis and an additional tick is being added in the very end, however the chart is drawn right.
If I add .nice(this.dataArray.length) to this.x = ..., the year 2010 is added with 2017 at the very end.
How can I fix this problem? Thank you.
You can map your data:
var ticks = data.map((d)=>d);
And use this array in your tickValues:
var axis = d3.axisBottom(scale)
.tickValues(ticks);
Here is a demo:
var width = 500,
height = 100;
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
var parse = d3.timeParse("%Y-%m-%d");
var data = ["2010-12-31",
"2011-12-31",
"2012-12-31",
"2013-12-31",
"2014-12-31",
"2015-12-31",
"2016-12-31"
];
data.forEach((d, i, a) => a[i] = parse(d));
var ticks = data.map((d) => d)
var format = d3.timeFormat("%Y")
var scale = d3.scaleTime()
.domain(d3.extent(data, d => d)).range([20, width - 20]);
var axis = d3.axisBottom(scale).tickValues(ticks).tickFormat((d) => format(d));
var gX = svg.append("g")
.attr("transform", "translate(0,50)")
.call(axis);
<script src="https://d3js.org/d3.v4.min.js"></script>
I have a graph which shows live data over the past 90 minutes currently my x axis is labelled 90 to 0 using
xRRange = d3.scale.linear().range([MARGINS.left, WIDTH - MARGINS.right])
.domain([90,0])
What I want to do is replace the numbers 0 - 90 with times, ie the 90 would be now-90 and the 0 would be now
I have tried the following but it fails to draw the axis, any ideas where I have went wrong
var xStart = Math.round(+new Date()/1000);
var xEnd = xStart - (60*90);
xTRange = d3.scale.time().range([MARGINS.left, WIDTH - MARGINS.right])
.domain([xEnd, xStart])
.nice()
xTAxis = d3.svg.axis().scale(xTRange)
.tickSize(20)
.tickSubdivide(true);
vis.append('svg:g').attr('class', 'x axis')
.attr('transform', 'translate(0,' + (HEIGHT - MARGINS.bottom) + ')')
.call(xTAxis);
You seem to have swapped the xEnd and xStart. In your first code block, the smaller value is at the end. In your 2nd, the larger values is at the end.
Also, your first line converts the xStart and xEnd values into seconds. You don't convert them back to milliseconds in your domain call. When d3 coerces them into dates, they won't be what you expect (will be far lesser)
As #potatopeelings excellent answer states, JavaScript works with milliseconds. Also, a couple of other things. It's d3.time.scale, not d3.scale.time. And in your scale assignment, you need to reverse your xEnd and xStart.
Here it all is working:
<!DOCTYPE html>
<html>
<head>
<script data-require="d3#3.5.3" data-semver="3.5.3" src="//cdnjs.cloudflare.com/ajax/libs/d3/3.5.3/d3.js"></script>
</head>
<body>
<script>
var vis = d3.select('body')
.append('svg')
.attr('width', 400)
.attr('height', 150);
var xStart = new Date();
var xEnd = xStart - (90 * 60000); // 60,000 milliseconds in minute
xTRange = d3.time.scale()
.range([20, 380])
.domain([xStart, xEnd])
.nice();
xTAxis = d3.svg.axis().scale(xTRange)
.tickSize(20)
.tickSubdivide(true);
vis.append('svg:g').attr('class', 'x axis')
.attr('transform', 'translate(0,' + 10 + ')')
.call(xTAxis);
</script>
</body>
</html>
I am using d3js to display a realtime representation of the views of a website. For this I use a stack layout and I update my dataset by JSON at the moment.
When there is only 1 or 2 views being displayed on the y axis, which is dynamic related to the amount of views in the graph, the axis labels are: 1 => 0, 0.2, 0.4, 0.6, 0.8, 1, the axis labels are: 2 => 0, 0.5, 1, 1.5, 2 This makes no sense for my dataset since it displays views of a page, and you can't have half a view.
I have a linear scale in d3js I base my y axis on
var y_inverted = d3.scale.linear().domain([0, 1]).rangeRound([0, height]);
According to the documentation of rangeRound() I should only get whole values out of this scale. For drawing my axis I use:
var y_axis = svg.append("g")
.attr("class", "y axis")
.attr("transform", "translate(0,0)")
.call(y_inverted.axis = d3.svg.axis()
.scale(y_inverted)
.orient("left")
.ticks(5));
Because it is a realtime application I update this every second by calling:
function update(){
y_inverted.domain([yStackMax, 0]);
y_axis.transition()
.duration(interval)
.ease("linear")
.call(y_inverted.axis);
}
yStackMax is calculated from a stacklayout, as far as I know the data used for the y values only contain integers.
var yStackMax = d3.max(layers, function(layer) {
return d3.max(layer, function(d) {
return d.y0 + d.y;
});
});
I have tried several things to get a proper value for my y axis.
d3.svg.axis()
.scale(y_inverted)
.orient("left")
.ticks(5).tickFormat(d3.format(",.0f"))
Got me the closest sofar, but it still displays 0, 0, 0, 1, 1, 1
Basically what I want is to only have 1 tick when yStackMax is 1, 2 ticks when it's 2, but it should also work if yStackMax is 12 or 1,000,000
Short answer: You can dynamically set the number of ticks. Set it to 1 to display only two tick labels:
var maxTicks = 5, minTicks = 1;
if (yStackMax < maxTicks) {
y_axis.ticks(minTicks)
}
else {
y_axis.ticks(maxTicks)
}
Long Answer (going a bit off topic):
While playing with your example I came up with a rather "complete solution" to all your formatting problems. Feel free to use it :)
var svg = d3.select("#svg")
var width = svg.attr("width")
var height = svg.attr("height")
var yStackMax = 100000
var interval = 500
var maxTicks = 5
var minTicks = 1
var y_inverted = d3.scale.linear().domain([0, 1]).rangeRound([0, height])
var defaultFormat = d3.format(",.0f")
var format = defaultFormat
var y_axis = d3.svg.axis()
.scale(y_inverted)
.orient("left")
.ticks(minTicks)
.tickFormat(doFormat)
var y_axis_root;
var decimals = 0;
function countDecimals(v){
var test = v, count = 0;
while(test > 10) {
test /= 10
count++;
}
return count;
}
function doFormat(d,i){
return format(d,i)
}
function init(){
y_axis_root = svg.append("g")
.attr("class", "y axis")
// I modified your example to move the axis to a visible part of the screen
.attr("transform", "translate(150,0)")
.call(y_axis)
}
// custom formatting functions:
function toTerra(d) { return (Math.round(d/10000000000)/100) + "T" }
function toGiga(d) { return (Math.round(d/10000000)/100) + "G" }
function toMega(d) { return (Math.round(d/10000)/100) + "M" }
function toKilo(d) { return (Math.round(d/10)/100) + "k" }
// the factor is just for testing and not needed if based on real world data
function update(factor){
factor = (factor) || 0.1;
yStackMax*=factor
decimals = countDecimals(yStackMax)
console.log("yStackMax decimals:",decimals, factor)
if (yStackMax < maxTicks) {
format = defaultFormat
y_axis.ticks(minTicks)
}
else {
y_axis.ticks(maxTicks)
if (decimals < 3 ) format = defaultFormat
else if(decimals < 6 ) format = toKilo
else if(decimals < 9 ) format = toMega
else if(decimals < 12) format = toGiga
else format = toTerra
}
y_inverted.domain([yStackMax, 0]);
y_axis_root.transition()
.duration(interval)
.ease("linear")
.call(y_axis);
}
init()
setTimeout(update, 200)
setTimeout(update, 400)
setTimeout(update, 600)
You can try it together with this html snippet:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<script type="text/javascript" src="http://mbostock.github.com/d3/d3.v2.js"></script>
</head>
<body>
<div><svg id="svg" width="200" height="300"></svg></div>
<script src="axis.js"></script>
<button id="button1" onclick="update(10)">+</button>
<button id="button2" onclick="update(0.1)">-</button>
</body>
</html>
I know it is a bit off topic but I usually like to provide running examples/solutions. Regard the additional formatting stuff as a bonus to the actual problem.
If you ask for a certain number of ticks (via axis.ticks() ) then d3 will try to give you that many ticks - but will try to use pretty values. It has nothing to do with your data.
Your solutions are to use tickFormat, as you did, to round all the values to integer values, only ask for one tick as Juve answered, or explicitly set the tick values using axis.tickValues([...]) which would be pretty easy used in conjunction with d3.range
rangeRound will not help in this case because it relates to the output range of the scale, which in this case is the pixel offset to plot at: between 0 and height.
Going off of Superboggly's answer, this is what worked for me. First I got the max (largest) number from the y domain using y.domain().slice(-1)[0] and then I built an array of tick values from that using d3.range()...
var y_max = y.domain().slice(-1)[0]
var yAxis = d3.svg.axis()
.scale(y)
.tickValues(d3.range(y_max+1))
.tickFormat(d3.format(",.0f"))
Or just let the ticks as they are and "hide" decimal numbers
d3.svg.axis()
.scale(y_inverted)
.orient("left")
.ticks(5).tickFormat(function(d) {
if (d % 1 == 0) {
return d3.format('.f')(d)
} else {
return ""
}
});
Here is the code:
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.tickFormat(d3.format(".2s"));