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>
Related
I've spent several days trying to create a real-time timeline where data is appended asynchronously. It seems impossible to get things working smoothly without breaking something else each time.
I had a look at several examples but none of them seem to match my case. Namely, most of the real-time examples either rely on the data to increment the timeline by a step each time, or either they all assume that the data comes continuously in regular intervals.
Issues I'm having:
Points slide nicely. However if I switch a tab and switch back they continue from where they left, and thus not matching the current ticks in the x axis
The ticks in the timeline every now and then get some weird transition that looks like shuffling. After the shuffling the actual points are out of sync with the timeline.
Here's a fiddle
<!doctype html><html lang="en">
<head><script src="https://d3js.org/d3.v5.min.js"></script></head>
<body><script>
const LENGTH = 10 // in seconds
const TICKS = 10 // num of ticks in time axis
const HEIGHT = 240
const WIDTH = 950
const MARGIN_LEFT = 40
const MARGIN_TOP = 40
var datapoints = []
// Create root element + background rect
svg = d3.select('body').append('svg')
.attr('width', WIDTH)
.attr('height', HEIGHT)
svg.append('rect')
.attr('width', '100%')
.attr('height', '100%')
.attr('fill', 'rgba(59, 58, 52, 0.8)')
$graphs = svg.append('g')
$slidables = $graphs.append('g')
// We use two scalers for x. This solves the issue with the axis being out
// of sync
scaleX = d3.scaleTime().range([MARGIN_LEFT, WIDTH-MARGIN_LEFT])
scaleY = d3.scaleLinear().range([MARGIN_TOP, HEIGHT-MARGIN_TOP])
updateTimeScale()
// -------------------------------------------------------------------------
function logDate(date){
console.log(date.toTimeString().split(' GMT')[0] + ' ' + date.getMilliseconds() + 'ms')
}
function logPoint(point){
const date = point[0]
console.log(
date.toTimeString().split(' GMT')[0] + ' ' + date.getMilliseconds() + 'ms, ',
point[1]
)
}
function oneSecondAgo(){
d = new Date()
d.setSeconds(d.getSeconds() - 1)
return d
}
function leftDate(){
d = new Date()
d.setSeconds(d.getSeconds() - LENGTH)
return d
}
function tickDist(){
return scaleX(new Date()) - scaleX(oneSecondAgo())
}
// -------------------------------- Init -----------------------------------
/* Resets timescale to the current time */
function updateTimeScale(){
right = new Date()
left = new Date()
right.setSeconds(right.getSeconds())
left.setSeconds(right.getSeconds()-LENGTH)
scaleX.domain([left, right])
}
function init(){
// Setup axis
xaxis = d3.axisBottom(scaleX).ticks(TICKS)
$xaxis = svg.append('g')
.attr('transform', 'translate(0, ' + (HEIGHT - MARGIN_TOP) + ')')
.call(xaxis)
yaxis = d3.axisLeft(scaleY)
$yaxis = svg.append('g')
.attr('transform', 'translate(' + MARGIN_LEFT + ', 0)')
.call(yaxis)
// Garbage collect old points every second
setInterval(function(){
while (datapoints.length > 0 && scaleX(datapoints[0][0]) <= MARGIN_LEFT){
datapoints.shift()
}
$slidables.selectAll('circle')
.data(datapoints, d=>d)
.exit()
.remove()
}, 1000)
// Slide axis at interval
function tick(){
right = new Date()
left = new Date()
right.setSeconds(right.getSeconds()+1)
left.setSeconds(right.getSeconds()-LENGTH)
scaleX.domain([left, right])
$xaxis.transition()
.ease(d3.easeLinear)
.duration(new Date() - oneSecondAgo())
.call(xaxis)
}
tick()
setInterval(tick, 1000)
}
// ------------------------------ Update -----------------------------------
/* Update graph with points
We always set right edge to current time
*/
function update(points){
datapoints = datapoints.concat(points)
logPoint(points[0])
updateTimeScale()
// Add new points, transition until left edge and then remove
$slidablesEnter = $slidables.selectAll('circle')
.data(datapoints, d=>d)
.enter()
$slidablesEnter
.append("circle")
.style("fill", "rgb(74, 255, 0)")
.attr("r", 2)
.attr("cx", p=>scaleX(p[0])) // put it at right
.attr("cy", p=>scaleY(p[1]))
.transition()
.duration(function(p){
remainingTime = p[0] - leftDate()
return remainingTime
})
.ease(d3.easeLinear)
.attr("cx", p => MARGIN_LEFT)
.remove()
}
// Start everything with two random datapoints
init()
d1 = new Date()
d2 = new Date()
d2.setMilliseconds(d2.getMilliseconds()-1500)
update([[d1, Math.random()]])
update([[d2, Math.random()]])
</script></body></html>
Just some updates. My workaround is to repaint everything on a tiny interval (i.e. 20ms).
This solves all the syncing issues but not sure if there will be a difference on performance.
Something like this
function tick(){
// Redraw all elements
scaleX.domain([now(-TIMELINE_LENGTH_MS), now()])
$slidables.selectAll('circle')
.data(datapoints, d=>d)
.attr("cx", p => scaleX(p[0]))
.transition()
.duration(0)
}
setInterval(tick, REFRESH_INTERVAL)
Setting REFRESH_INTERVAL to 20ms, looks pretty much the same as having a transition. Anything above starts looking chopppy, but at least is more accurate than before.
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>
I am using this example to create my own real-time graph using d3. In my version the graph is initialized with existing data. Problem is, the x-axis initialization causes a very small portion of the graph to show while it is transitioning or collapsing on the right before finally showing the normal scale and resultantly the normal graph. I am pretty sure the axis is causing it because the moment the axis returns to normal so does the graph. Is there a way to remove this transition at the begging or otherwise have it not skew the graph or not show until it is ready? Here is the problem in action, better than me trying to explain it: http://codepen.io/Dordan/pen/NbBjPB/
Here is the code snippet for creating the x-axis:
var limit = 60 * 1;
var duration = 750;
var now = new Date(Date.now() - duration);
var x = d3.time.scale().domain([now - (limit - 2), now - duration]).range([0, width]);
var axis = svg.append('g')
.attr('class', 'x-axis')
.attr('transform', 'translate(0,' + height + ')')
.call(x.axis = d3.svg.axis().scale(x).orient('bottom'));
The instantiation of your x scale is missing the '* duration' when you're calculating the domain. Use this instead and it works well:
var x = d3.time.scale().domain([now - (limit - 2) * duration, now - duration]).range([0, width]);
I found this thread and it got me halfway to where I need to be and I'm wondering if anyone knows how I can adjust the solution to fit my needs.
So if I have some values in the thousands, and some values in the millions, does anyone know how I can set all of the ticks to be formatted in the millions? For instance, if I have a value for 800k it would show up as 0.8million instead.
This is what I get when using the above solution without adjusting it.
You don't need to use a SI prefix in this case. Given this domain:
var scale = d3.scaleLinear().domain([200000,1800000])
Going from 200 thousand to 1.8 million, you can simply divide the tick value by 1,000,000 and add a "million" string.
Here is the demo:
var svg = d3.select("body")
.append("svg")
.attr("width", 600)
.attr("height", 100);
var scale = d3.scaleLinear().domain([200000,1800000]).range([20,550]);
var axis = d3.axisBottom(scale).tickFormat(function(d){return d/1000000 + " Million"});
var gX = svg.append("g").attr("transform", "translate(20, 50)").call(axis);
<script src="https://d3js.org/d3.v4.min.js"></script>
EDIT: According to the comments, you want to show "million" only in the last tick. Thus, we have to check if the tick is the last one and conditionally formatting it:
var axis = d3.axisBottom(scale).tickFormat(function(d){
if(this.parentNode.nextSibling){
return d/1000000;
} else {
return d/1000000 + " Million";
}
});
Here is the demo:
var svg = d3.select("body")
.append("svg")
.attr("width", 600)
.attr("height", 100);
var scale = d3.scaleLinear().domain([200000,1800000]).range([20,550]);
var axis = d3.axisBottom(scale).tickFormat(function(d){
if(this.parentNode.nextSibling){
return d/1000000} else { return d/1000000 + " Million"}});
var gX = svg.append("g").attr("transform", "translate(20, 50)").call(axis);
<script src="https://d3js.org/d3.v4.min.js"></script>
Given the following speed dial, which is constructed using arcs in D3:
segmentArc = d3.svg.arc().outerRadius(radius - chartInset).innerRadius(radius - chartInset - barWidth).startAngle(arcStartRad + startPadRad).endAngle(arcEndRad - endPadRad);
How do I move the labels in each segment so that it appears right justified (at the end of each segment opposed to center)?
the labels are currently added likes this:
chart.append('text')
.attr('transform', () => {
var x = Math.round(segmentArc.centroid()[0]);
var y = Math.round(segmentArc.centroid()[1]);
return 'translate(' + x + ',' + y + ')';
})
.style("text-anchor", "middle")
.text(sectionLabel);
I solved this problem by replicating each segment arc, giving it a transparent fill and making it exactly twice as long. From there it is as simple as working out the centroid for the transparent arc.