I'm trying to draw a line using D3.js. They are samples taken at intervals over a period of time. I want to draw them with a time axis for x. Each point of data is just an index in an array and I can't figure out how to set up my axis in such a way that I don't have to manually re-scale the axis before calling d3.time.scale.
Does anyone know how to clean up the scale?
Snippets out of my code. My actual code downloads the data and draws a lot of lines over different time periods with different offsets translated in the graph.
// input data
var start_time = 1352684763;
var end_time = 1352771163;
// data is exactly 100 samples taken between start_time and end_time
var data = [140,141,140,140,139,140,140,140,140,141,139,140,54,0,0,0,0,0,0,0,0,0...]
var y_max = d3.max(data);
// graph
var scale_x = d3.time.scale().domain([start_time, end_time]).range([0, 100]);
var scale_y = d3.scale.linear().domain([0, y_max]).range([height, 0]);
var step = (end_time - start_time)/100;
function re_scale(x) { return start_time + x*step; }
// for x, rescale i (0..99) into a timestamp between start_time and end_time before returning it and letting scale_x scale it to a local position. Awkward.
var line = d3.svg.line()
.x(function(d, i) { return scale_x(re_scale(i)); })
.y(scale_y)
.interpolate('basis')
var g = graph.selectAll("g")
.append('svg:path')
.attr('d', function(d) { return line(data); })
// also draw axis here...
The "domain" should refer to the span in the data, and the "range" should refer to the span on the screen.
At the moment it would be interpreting .range([0, 100]) on scale_x as a number of pixels. If you change this to .range([0, width]) it should work without needing to re-scale.
d3.time.scale() only needs to know the start and end points to produce a good axis. However if you do want a tick for every data point there are options do do this in the docs.
Related
I'm new to d3 and have the following code for creating the x-axis on my graph:
export const drawXAxis = (svg, timestamps, chartWidth, chartHeight) => {
console.log(chartWidth); // 885
console.log(timestamps.length); // 310
const xScale = d3.scaleLinear()
.domain([-1, timestamps.length])
.range([0, chartWidth]);
const xBand = d3.scaleBand()
.domain(
d3.range(-1, timestamps.length))
.range([0, chartWidth])
.padding(0.3);
const xAxis = d3.axisBottom()
.scale(xScale)
.tickFormat(function(d) {
const ts = moment.utc(timestamps[d]);
return ts.format('HH') + 'h';
});
const gX = svg.append("g")
.attr("class", "axis x-axis")
.attr("transform", "translate(0," + chartHeight + ")")
.call(xAxis);
return [xScale, xBand, xAxis, gX];
};
As I understand it, d3 decides on the number of ticks that appears on the X-axis.
In order to gain more control over the values appearing on the X-axis for zooming purposes, I would like to understand how d3 determines that - in this case - I have 16 ticks.
What If I want to space the ticks more evenly, for example, I want to see a tick on every 12 or 6 hours? My data contains 0 -> 23 hour values per day consistently, but d3 displays random hours on my graph.
I'm gonna answer just the question in the title ("how is the number of ticks on an axis defined?"), not the one you made at the end ("What If I want to space the ticks more evenly, for example, I want to see a tick on every 12 or 6 hours?"), which is not related and quite simple to fix (and, besides that, it's certainly a duplicate).
Your question demands a detective work. Our journey starts, of course, at d3.axisBottom(). If you look at the source code, you'll see that the number of ticks in the enter selection...
tick = selection.selectAll(".tick").data(values, scale).order()
...depends on values, which is:
var values = tickValues == null ? (scale.ticks ? scale.ticks.apply(scale, tickArguments) : scale.domain()) : tickValues
What this line tells us is that, if tickValues is null (no tickValues used), the code should use scale.ticks for scales that have a ticks method (continuous), our just the scale's domain for ordinal scales.
That leads us to the continuous scales. There, using a linear scale (which is the one you're using), we can see at the source code that scale.ticks returns this:
scale.ticks = function(count) {
var d = domain();
return ticks(d[0], d[d.length - 1], count == null ? 10 : count);
};
However, since ticks is imported from d3.array, we have to go there for seeing how the ticks are calculated. Also, since we didn't pass anything as count, count defaults to 10.
So, finally, we arrive at this:
start = Math.ceil(start / step);
stop = Math.floor(stop / step);
ticks = new Array(n = Math.ceil(stop - start + 1));
while (++i < n) ticks[i] = (start + i) * step;
Or this:
start = Math.floor(start * step);
stop = Math.ceil(stop * step);
ticks = new Array(n = Math.ceil(start - stop + 1));
while (++i < n) ticks[i] = (start - i) / step;
Depending on the value of steps. If you look at the tickIncrement function below, you can see that steps can only be 1, 2, 5 or 10 (and their negatives).
And that's all you need to know the length of the array in the variable ticks above. Depending on the start and stop values (i.e., depending on the domain), sometimes we have more than 10 ticks (16 in your case), sometimes we have less than 10, even if the default count is 10. Have a look here:
const s = d3.scaleLinear();
console.log(s.domain([1,12]).ticks().length);
console.log(s.domain([100,240]).ticks().length);
console.log(s.domain([10,10]).ticks().length);
console.log(s.domain([2,10]).ticks().length);
console.log(s.domain([1,4]).ticks().length);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
The last example, as you can see, gives us 16 ticks.
I trying to create a function to draw a simple line chart with CO2 emission figures for different cities. In my dataset, every column is a city and every row is a year, so the figures refer to the CO2 released to the atmosphere for every year in that place.
function lineChart(city){
// set the dimensions and margins of the graph
var margin = {top: 20, right: 20, bottom: 30, left: 50},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
// parse the date / time
var parseTime = d3.timeParse("%Y");
// set the ranges
var x = d3.scaleTime().range([0, width]);
var y = d3.scaleLinear().range([height, 0]);
// define the line
var valueline = d3.line()
.x(function(d) { return x(d.Year); })
.y("function(d) { return y(d."+city+"); }");
;}
lineChart("london");
I know the code is incomplete but I pasted this bit to let you know what I want to do. I want to draw different graphs for each city by calling the function with the city as the variable. I tried with this .y("function(d) { return y(d."+city+"); }"); but it is not working.
Does anyone know how to fix this problem?
Thanks
You don't need the " around the "function(d) { return y(d."+city+"); }" -- that tells javascript to interpret the text function(d) { return y(d. and ); } as a string, giving a y attribute of function(d) { return y(d.london); }. You want to perform the function, rather than just quote it.
To access the data for the city, you need to access
d['london']
from the object d (it can also be written as d.london). Since city is a variable storing the name of the current city, you can do this using
d[city]
The whole line is thus
.y(function(d) { return y(d[city]); });
I am making a multi-line chart and I have implemented a brush to be able to zoom into a specific domain on the x-axis. However, when I zoom in I want the y-axis to scale along so that its domain goes from [0, maxY], where maxY is the maximum y-value for the current selection on the x-axis. To generate the lines I am using d3.line() (which has the connection between the x and y values). This is how I currently calculate the maxY value:
//Find max and min values in data to set the domain of the y-axis
var maxArray = updatedData.map(function(variable){
//First map the values in the array to a new array
var valuesArray = variable.values.map(function(d){
return d.value;
})
//Find max value in array
return Math.max(...valuesArray);
});
var maxY = Math.max(...maxArray);
And here is where I set the scales and create the d3.line():
var xScale = d3.scaleTime()
.range([0, chartWidth]);
var yScale = d3.scaleLinear()
.domain([0, maxY])
.range([chartHeight, 0]);
var brush = d3.brushX()
.on("end", brushend);
var line = d3.line()
.curve(d3.curveBasis)
.x(function(d) {return xScale(d.date)})
.y(function(d) {return yScale(d.value)})
//Save this to be able to zoom back out
var originalDomain = [new Date(data[0].Timestamp), new Date(data[data.length-1].Timestamp)];
xScale.domain(originalDomain);
Here is the code where I set the new xScale.domain() and zoom in on that interval (which is called when the brushing is ended):
function brushend(){
//sourceEvent - the underlying input event, such as mousemove or touchmove.
if (d3.event.sourceEvent && d3.event.sourceEvent.type === "brush") return; // ignore zoom-by-brush
var brushInterval = d3.event.selection; //The interval of the current brushed selection
//If the function is called with no selection: ignore
if(!brushInterval) return;
//Enable reset button
resetButton.attr("disabled", null)
.on("click", resetAxis);
var newDomain = brushInterval.map(xScale.invert, xScale);
//TODO: Find max and min values in data to set the domain of the y-axis
xScale.domain(newDomain);
chart.selectAll(".line")
.transition()
.duration(1000)
.attr("d", function(d){ return line(d.values)});
chart.select(".x-axis")
.transition()
.duration(1000)
.call(xAxis);
//Remove the visual brush
d3.select(".brush").call(brush.move, null);
}
What I would like to do is to find the maximum y-value in the currently selected domain. I know that I can filter the data values to remove the ones that are not in the currently selected domain and then calculate the maximum value from them (like I did for the original domain). But it seems like there should be an easier solution to this. I didn't find any function in the documentation for d3.line() that could calculate max values.
Is there any easy way to calculate max value from d3.line()?
Thanks
There is not really an easier solution to this as you somehow have to filter the values to only take into account the ones which are in your selected x domain. However, using two nested calls to d3.max() you can at least give it a pleasant look and spare some iterations by avoiding an additional call to .filter(). Since d3.max() will ignore null values you can use it to filter your values by returning null if the current datum is outside of the x domain's boundaries. To get the maximum value you can use something like the following:
const maxY = xDomain => d3.max(updatedData, variable =>
d3.max(
variable.values,
v => v.Timestamp >= xDomain[0] && v.Timestamp <= xDomain[1] ? v.value : null
)
);
Have a look at the following snippet for a working demo:
var updatedData = [{
values: [{Timestamp:0, value:1},{Timestamp:1, value:5},{Timestamp:2, value:10},{Timestamp:3, value:3},{Timestamp:4, value:30}]
}, {
values: [{Timestamp:0, value:19},{Timestamp:1, value:12},{Timestamp:2, value:13},{Timestamp:3, value:8},{Timestamp:4, value:50}]
}];
const maxY = xDomain => d3.max(updatedData, variable =>
d3.max(
variable.values,
v => (!xDomain || v.Timestamp >= xDomain[0] && v.Timestamp <= xDomain[1]) ? v.value : null
)
);
console.log(maxY()); // Default, check all values: max 50
console.log(maxY([1,3])); // Max 13
console.log(maxY([0,3])); // Max 19
<script src="https://d3js.org/d3.v4.js"></script>
I'm trying to draw earthquakes over a period of few months on a map. With the following code I've drawn all the earthquake occurences but I want to scale the time period and make it video-like.
svg.selectAll(".shock")
.data(data)
.enter().append("circle")
.attr("class", 'shock')
.attr("r", 5)
.attr("transform", function(d) {
return "translate(" + projection([
d.lng,
d.lat
]) + ")";
});
How do I show all these earthquakes in a 15 second animation?
One way I've thought is: append all circles (date-sorted) with radius 0, then increase the radius of the first one, and setTimeout to increase the radius of the next one and so on.. Is there any better way?
I know about time scales, but don't know how to use them to draw over time?
First scale your min and max time (using .getTime()) over the animation duration
var timeScale = d3.scale.linear()
.domain([min, max])
// a one second delay before we start + 14 seconds of earthquakes
.range([1000, 15000])
Then just set timeouts to display the points according to the scaled time
...
.attr("display", function (d) {
// set a timeout to show this at the right time
var self = this;
setTimeout(function () {
d3.select(self).attr("display", "block")
}, timeScale((new Date(d.When)).getTime()));
return "none";
})
Fiddle - http://jsfiddle.net/4c1ukb1z/ - you'll need to update the projections and the When attribute parsing according to your code / data structure.
Of course, it would be possible to do this using timescales too if you are getting date time objects.
I am working on a chart looking like this now:
I use d3 scales and ranges to setup sizes and coordinates of circles, from JSON data.
All works fine but I need to make sure those circles that are close to extreme values don't overlap the sides of the chart (like orange circle on the top right and blue one on the bottom side), so I think I need to play with ranges and change coordinates in case they overlap or is there a better tried way to do this?
When drawing circles, in addition to the x and y scaling functions we also use an r scaling function:
var rScale = d3.scale.linear()
.domain([0, maxR])
.range([0, maxBubbleRadius]);
var xScale = d3.scale.linear()
.domain([minX, maxX])
.range([0, chartWidth]);
var yScale = d3.scale.linear()
.domain([minY, maxY])
.range([chartHeight, 0]);
where maxR is the largest r value in your dataset and maxBubbleRadius is however large you want the largest circle to be, when you plot it.
Using the x and y scaling functions it is easy to calculate where the centre of each circle will be plotted, we can then add on the (scaled) r value to see if the circle will spill over a chart boundary. With a scenario like the first chart below we can see that 4 of the circles spill over. The first step to remedy this is to find out how many vertical and horizontal units we spill over by and then increase the minimum and maximum x and y values to take this into account, before recalculating the xScale and yScale vars. If we were to then plot the chart again, the boundary would move out but there would probably still be some visible spillage (depending on actual values used); this is because the radius for a given circle is a fixed number of pixels and will therefore take up a different number of x and y units on the chart, from when we initially calculated how much it spilled over. We therefore need to take an iterative approach and keep applying the above logic until we get to where we want to be.
The code below shows how I iteratively achieve an acceptable scaling factor so that all the circles will plot without spilling. Note that I do this 10 times (as seen in the loop) - I've just found that this number works well for all the data that I've plotted so far. Ideally though, I should calculate a delta (the amount of spillage) and iterate until it is zero (this would also require overshooting on the first iteration, else we'd never reach our solution!).
updateXYScalesBasedOnBubbleEdges = function() {
var bubbleEdgePixels = [];
// find out where the edges of each bubble will be, in terms of pixels
for (var i = 0; i < dataLength; i++) {
var rPixels = rScale(_data[i].r),
rInTermsOfX = Math.abs(minX - xScale.invert(rPixels)),
rInTermsOfY = Math.abs(maxY - yScale.invert(rPixels));
var upperPixelsY = _data[i].y + rInTermsOfY;
var lowerPixelsY = _data[i].y - rInTermsOfY;
var upperPixelsX = _data[i].x + rInTermsOfX;
var lowerPixelsX = _data[i].x - rInTermsOfX;
bubbleEdgePixels.push({
highX: upperPixelsX,
highY: upperPixelsY,
lowX: lowerPixelsX,
lowY: lowerPixelsY
});
}
var minEdgeX = d3.min(bubbleEdgePixels, function(d) {
return d.lowX;
});
var maxEdgeX = d3.max(bubbleEdgePixels, function(d) {
return d.highX;
});
var minEdgeY = d3.min(bubbleEdgePixels, function(d) {
return d.lowY;
});
var maxEdgeY = d3.max(bubbleEdgePixels, function(d) {
return d.highY;
});
maxY = maxEdgeY;
minY = minEdgeY;
maxX = maxEdgeX;
minX = minEdgeX;
// redefine the X Y scaling functions, now that we have this new information
xScale = d3.scale.linear()
.domain([minX, maxX])
.range([0, chartWidth]);
yScale = d3.scale.linear()
.domain([minY, maxY])
.range([chartHeight, 0]);
};
// TODO: break if delta is small, rather than a specific number of interations
for (var scaleCount = 0; scaleCount < 10; scaleCount++) {
updateXYScalesBasedOnBubbleEdges();
}
}