I am trying to create a scatter plot with unequal intervals on the X-axis using d3.js. My CSV data is shown here partially:
chr,pos,val
22,8947,8.58891099252
22,8978,4.65541559632
22,8996,6.33685790218
22,8997,9.00384002282
22,9006,4.39533823989
MT,9471,5.0655064583
MT,9472,7.83798949399
MT,9473,0.587797595352
MT,9474,4.6475160648
MT,9475,2.52382097771
MT,9476,7.8431366396
MT,9477,1.71519736769
MT,9478,2.61168595179
MT,9479,4.15061022346
MT,9470,7.1477707428
The number of pos values for each chr value may be different. In some cases, it could be 20, in others 100 and so on. I need to create a plot of val on the y-axis vs chr on the x-axis, with the x-interval for each chr being equal to the number of pos values for that chr. Although ordinal scale for the x-axis seems suitable here, it probably doesn't support unequal intervals. With linear scale, unequal intervals can be shown using polylinear scales, but the presence of alphabetic characters in chr mean no ticks are shown. Does anyone know how I can show unequal intervals in d3.js?
UPDATE:
I have some code here for the domain and ticks using a linear scale:
const x = d3.scale.linear()
.domain(input.map((d) => {
if (d.chr === 'MT') {
return 23;
}
if (d.chr === 'X') {
return 24;
}
return d.chr;
}))
.range(xTicks);
I can't understand how to show the ticks now.With this it shows 23 and 24 instead of MT and X.
I am not sure of this part:
const xAxis = d3.svg.axis().scale(x).orient('bottom').tickValues(input.map((d) => {
if (d.chr === 'MT') {
// returning a string here shows NaN
return 23;
}
if (d.chr === 'X') {
return 24;
}
return d.chr;
}));
Here is an example of how conditionally formatting the ticks using tickFormat (not tickValues).
Suppose the data is:
19, 20, 21, 22, 23, 24;
But we are going to change 23 for "X" and 24 for "MT" in the ticks. Click "run code snippet":
var data = [19, 20, 21, 22, 23, 24];
var width = 400, height = 100;
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("heigth", height);
var xScale = d3.scale.linear()
.domain(d3.extent(data))
.range([0, width*.9]);
var xAxis = d3.svg.axis()
.orient("bottom")
.ticks(6)
.tickFormat(function(d){
if(d == 23){
return "X"
} else if(d==24){
return "MT"
} else {
return d
}
})
.scale(xScale);
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(20,20)")
.call(xAxis);
.axis path,
.axis line {
fill: none;
stroke: #aaa;
shape-rendering: crispEdges;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
I started at chromosome 19 just to save some space, but you can get the general idea.
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>
Question
I have positive and negative values, and I'd like to plot them on a "logarithmic" scale.
Imagine a scale with evenly spaced ticks for the following values:
-1000, -100, -10, -1, 0, 1, 10, 100, 1000
I want 0 in there, which is defined to be -Inf by logarithms, complicating this further.
However, I don't think this request is unreasonable. This seems like a sensible scale any data scientist might want to plot strongly divergent values against.
How do you create such a scale and axis in d3?
Thoughts
It might be possible to do this cleverly with 2 d3.scaleLog()s or maybe 3 scales if you use a technique like this one.
I was hoping there might be an easy way to fit this in a d3.scalePow() with .exponent(0.1) but unless I've got my log rules mixed up, you can't get a .scaleLog() out of a .scalePow() (though you can probably approximate it okay for some ranges).
We can't have a true log scale like this, or even a combination of two log scales like this as. We need to set a cut off for zeroish values, and this is where error might be introduced depending on your data. Otherwise, to make a scale function like this is fairly straightforward, just call a different scale for negative and positive while setting zero-ish values to zero.
This combination of scales might look like:
var positive = d3.scaleLog()
.domain([1e-6,1000])
.range([height/2,0])
var negative = d3.scaleLog()
.domain([-1000,-1e-6])
.range([height,height/2])
var scale = function(x) {
if (x > 1e-6) return positive(x);
else if (x < -1e-6) return negative(x);
else return height/2; // zero value.
}
And an example:
var width = 500;
var height = 300;
var positive = d3.scaleLog()
.domain([1e-1,1000])
.range([height/2,0])
var negative = d3.scaleLog()
.domain([-1000,-1e-1])
.range([height,height/2])
var scale = function(x) {
if (x > 1e-6) return positive(x);
else if (x < -1e-6) return negative(x);
else return height/2; // zero value.
}
var line = d3.line()
.y(function(d) { return scale(d) })
.x(function(d,i) { return (i); })
var svg = d3.select("body")
.append("svg")
.attr("width",width)
.attr("height",height)
var data = d3.range(width).map(function(d) {
return (d - 250) * 4;
})
svg.append("path")
.attr("d", line(data) );
path {
fill: none;
stroke: steelblue;
stroke-width: 2px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
Creating a single scale
The above is a proof of concept maybe.
Now the trickier part is making an axis. We could make axes for both the scales above, leaving zero with some sort of manual correction. But it will be easier to create a scale with our own interpolotor using the above as an exmaple. This gives us one scale which we can create an axis for. Our interpolator might look like:
// Interpolate an output value:
var interpolator = function(a,b) {
var y0 = a;
var y1 = b;
var yd = b-a;
var k = 0.0001;
var positive = d3.scaleLog()
.domain([k,1])
.range([(y0 + y1)/2 ,y1])
var negative = d3.scaleLog()
.domain([-1,-k])
.range([y0, (y1 + y0)/2])
return function(t) {
t = (t - 0.5) * 2; // for an easy range of -1 to 1.
if (t > k) return positive(t);
if (t < -1 + k) return y0;
if (t < -k) return negative(t);
else return (y0 + y1) /2;
}
}
And then we can apply that to a regular old d3 linear scale:
d3.scaleLinear().interpolate(interpolator)...
This will interpolate numbers in the domain to the range as we've specified. It largely takes the above and adopts it for use as a d3 interpolator: a,b are the domain limits, t is a normalized domain between 0 and 1, and k defines the zeroish values. More on k below.
To get the ticks, assuming an nice round domain that only has nice round base ten numbers we could use:
// Set the ticks:
var ticks = [0];
scale.domain().forEach(function(d) {
while (Math.abs(d) >= 1) {
ticks.push(d); d /= 10;
}
})
Applying this we get:
var margin = {left: 40, top: 10, bottom: 10}
var width = 500;
var height = 300;
var svg = d3.select("body")
.append("svg")
.attr("width",width+margin.left)
.attr("height",height+margin.top+margin.bottom)
.append("g").attr("transform","translate("+[margin.left,margin.top]+")");
var data = d3.range(width).map(function(d) {
return (d - 250) * 4;
})
// Interpolate an output value:
var interpolator = function(a,b) {
var y0 = a;
var y1 = b;
var yd = b-a;
var k = 0.0001;
var positive = d3.scaleLog()
.domain([k,1])
.range([(y0 + y1)/2 ,y1])
var negative = d3.scaleLog()
.domain([-1,-k])
.range([y0, (y1 + y0)/2])
return function(t) {
t = (t - 0.5) * 2; // for an easy range of -1 to 1.
if (t > k) return positive(t);
if (t < -1 + k) return y0;
if (t < -k) return negative(t);
else return (y0 + y1) /2;
}
}
// Create a scale using it:
var scale = d3.scaleLinear()
.range([height,0])
.domain([-1000,1000])
.interpolate(interpolator);
// Set the ticks:
var ticks = [0];
scale.domain().forEach(function(d) {
while (Math.abs(d) >= 1) {
ticks.push(d); d /= 10;
}
})
// Apply the scale:
var line = d3.line()
.y(function(d) { return scale(d) })
.x(function(d,i) { return (i); })
// Draw a line:
svg.append("path")
.attr("d", line(data) )
.attr("class","line");
// Add an axis:
var axis = d3.axisLeft()
.scale(scale)
.tickValues(ticks)
svg.append("g").call(axis);
.line {
fill: none;
stroke: steelblue;
stroke-width: 2px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
Modifying the k value
Ok, what's up about k. It is needed to set the zeroish values. k also changes the shape of the graph. If showing regularly spaced ticks, increasing k ten fold increases the magnitude of the minimum magnitude ticks (other than zero) ten fold. In my exmaples above multiplying k by ten pushes the ticks with magnitude one overtop of the zero tick. Dividing it by ten would create room for a 0.1 tick (of course that requires modifying the tick generator to show that tick). k is hard to explain so I hope I did ok there.
I'll demonstrate to try and communicate it a bit better. Let's set the minimum magnitude ticks to be 0.1 using the above, we'll want to modify the tick function and k:
var margin = {left: 40, top: 10, bottom: 10}
var width = 500;
var height = 300;
var svg = d3.select("body")
.append("svg")
.attr("width",width+margin.left)
.attr("height",height+margin.top+margin.bottom)
.append("g").attr("transform","translate("+[margin.left,margin.top]+")");
var data = d3.range(width).map(function(d) {
return (d - 250) * 4;
})
// Interpolate an output value:
var interpolator = function(a,b) {
var y0 = a;
var y1 = b;
var yd = b-a;
var k = 0.00001;
var positive = d3.scaleLog()
.domain([k,1])
.range([(y0 + y1)/2 ,y1])
var negative = d3.scaleLog()
.domain([-1,-k])
.range([y0, (y1 + y0)/2])
return function(t) {
t = (t - 0.5) * 2; // for an easy range of -1 to 1.
if (t > k) {return positive(t)};
if (t < -1 + k) return y0;
if (t < -k) return negative(t);
else return (y0 + y1) /2 //yd;
}
}
// Create a scale using it:
var scale = d3.scaleLinear()
.range([height,0])
.domain([-1000,1000])
.interpolate(interpolator);
// Set the ticks:
var ticks = [0];
scale.domain().forEach(function(d) {
while (Math.abs(d) >= 0.1) {
ticks.push(d); d /= 10;
}
})
// Apply the scale:
var line = d3.line()
.y(function(d) { return scale(d) })
.x(function(d,i) { return (i); })
// Draw a line:
svg.append("path")
.attr("d", line(data) )
.attr("class","line");
// Add an axis:
var axis = d3.axisLeft()
.scale(scale)
.tickValues(ticks)
.ticks(10,".1f")
svg.append("g").call(axis);
.line {
fill: none;
stroke: steelblue;
stroke-width: 2px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
If you have a domain of +/- 1000, and you want the minimum magnitude tick to be 1 (not including zero) you want a k of 0.0001, or 0.1/1000.
If positive and negative limits of the domain are different, then we would need two k values, one for the negative cut off and one for the positive.
Lastly,
k sets values that are zeroish, in my example, t values that are between -k and +k are set to be the same - 0. Ideally this won't be many values in the dataset, but if it is, you might get a line such as:
Each input value is different, but the there are many zero output values, producing a visual artifact due to the bounds of what I haved considered to be zeroish. If there is only one value in the zeroish bounds, like in my examples above (but not the above picture), we get a much nicer:
I'm trying to create a line graph with D3.js and I want my X axis to start from 1 instead of 0.
The code looks as follows:
var temp = [36.5, 37.2, 37.8, 38.2, 36.8, 36.5, 37.3, 38.2, 38.3, 37];
var x = d3.scale.linear().domain([0,temp.length]).range([0, w]);
var xAxis = d3.svg.axis().scale(x).tickSize(-h).tickSubdivide(false);
graph.append("svg:g")
.attr("class", "x axis")
.attr("transform", "translate(0," + h + ")")
.call(xAxis);
When I change this to:
var x = d3.scale.linear().domain([1,temp.length]).range([0, w]);
the scale get's edited but the graph starts outside of the graph itself.
I tried to use tickvalues but I can't get this to work.
How can I let my scale start from 1?
I'm assuming you are labeling your axis by using the index of the data.
When you set your domain starting at 1, you're actually just telling your chart to make the left-most part of your graph be the x-coordinate of the second datum (index 1).
When you actually create the chart, there is a datum (index 0, value 36.5) that is outside of your defined domain, and d3 uses linear extrapolation to determine where it should be placed, making it end up to the left of the start of your chart.
What you really want to do is start your domain at 0, so that the first datum is in your domain, but to reformat your tick labels so that they show the index incremented by 1.
You can use axis.tickFormat() to do this.
var xAxis = d3.svg.axis()
.scale(x)
.tickSize(-h)
.tickFormat(function(d) { return d + 1; })
Side note: you shouldn't specify .tickSubdivide(false), since:
That function expects a number, not a boolean, and false will be coerced to 0.
The default value is 0 anyways.
axis.tickSubdivide is deprecated and does nothing as of version 3.3.0
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"));
I have a graph implemented in NVD3, and I'm having serious trouble with it. NVD3 seems unable to handle datasets which contain large values. The graph is located here: http://brad.kizer.com.ng/. The code for the graph is as so:
nv.addGraph(function() {
// Get maximum and minimum Y values
var minY = 2 >> 30,
maxY = 0;
data.forEach(function (d) {
d.values.forEach(function (s) {
minY = Math.min(minY, s.y);
maxY = Math.max(maxY, s.y);
});
});
var chart = nv.models.stackedArea()
.forceY([0, 20])
.height($(container).closest('.chart').height())
.width($(container).closest('.chart').width());
(Object.prototype.toString.call(data) === '[object Array]') || (data = [data]);
d3.select(container)
.attr('width', $(container).closest('.chart').width())
.attr('height', 250)
.datum(data)
.transition().duration(500)
.call(chart);
nv.utils.windowResize(chart.update);
return chart;
});
I will appreciate any help so much, as this has kept me scratching my head for days.
Solved my problem. The issue was that the data provided for the Y-axis was strings, which made supposedly number addition become string concatenation:
"123" + "902" + "384" + "382" == "123902384382"; // (instead of 1791)
What I did was walk through the data and convert the strings to numbers.