I am building an area graph in d3.js.
For my y axis, I want to use a linear scale that extends from the minimal value to the maximal value of the data represented in the graph.
Using
y = d3.scale.linear().range([height, 0]),
yAxis = d3.svg.axis().scale(y).orient("left")
d3 shows ticks only for multiples of 10000.
I would like also to see ticks for the starting and the ending value. Is this possible? How?
The nice() approach is usually preferable, but if you do want to have explicit tick labels at the max and min values of your data, you can force the axis to include them, along with whatever default tick values would be created by the scale:
axis.tickValues( scale.ticks( 5 ).concat( scale.domain() ) );
scale.ticks(count) returns an array of approximately that many tick values from the scale's domain, rounded off to nice even numbers. scale.domain() returns the [min,max] domain array that you set based on the data. array.concat(array) concatenates one array onto the end of the other, and axis.tickValues(array) tells the axis to draw the ticks at those values specifically.
Live example: https://jsfiddle.net/zUj3E/671/
(function () {
//Set up SVG and axis//
var svg = d3.select("svg");
var width = window.getComputedStyle(svg[0][0])["width"];
width = parseFloat(width);
var height = window.getComputedStyle(svg[0][0])["height"];
height = parseFloat(height);
var margin = 50;
var scale = d3.scale.linear()
.domain([0.05, 0.95])
.range([0, height - 2*margin]);
var formatPercent = d3.format(".0%");
var axis = d3.svg.axis()
.scale(scale)
.orient("right")
.tickFormat(formatPercent);
axis.tickValues( scale.ticks( 5 ).concat( scale.domain() ) );
//set the axis tick values explicitly, as the array of ticks
//generated by the scale PLUS the max and min values from the scale domain
//concatenated into a single array
svg.append("g")
.attr("class", "y axis")
.attr("transform", "translate(" + [margin, margin]+")")
.call(axis);
})();
g.axis line, g.axis path {
fill:none;
stroke:royalblue;
shape-rendering:crispEdges;
}
g.axis text{
fill:royalblue;
font-family:sans-serif;
}
g.axis path {
stroke-width:2;
stroke:seagreen;
}
svg {
display: block;
width: 100vw;
height: 100vh;
}
body {
margin: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<svg></svg>
One way to do this is to extend the domain of a scale to be "nice", i.e. to include "round" values that will show up on the axis. You can do this by calling .nice() after setting the domain:
y = d3.scale.linear().range([height, 0]).domain(...).nice();
The alternative would be to specify the ticks explicitly, which is much more painful in general.
Hacky way
Before trying this approach just try with ticks, tickSize, etc. functions once.
To be frank, I went through lots of solutions, and unfortunately, none of them worked for me. Finally, I played with the CSS and get the job done.
Hiding the in-between elements between the first and last tick value.
// displaying only first and last x-axis tick.
#x-axis {
.tick {
display: none
&:first-child,
&:last-of-type {
display: block;
}
}
}
Related
Code link: https://plnkr.co/edit/jLkoMxdzArBBULHF80nb?p=preview
I have a data with some disperse values. It ranges from 61 to 1.2m.
How can I represent it in a Histogram in a way that makes sense?
Can I have the last bucket on d3 that is > 2000 for instance?
Something like this (greater than 5 minutes):
First of all you will need to arrange your data (if you haven't yet), where you just need to create a variable with the > 2000 values.
This is the way I did it (I started d3 last week and I don't have any previous knowledge on JavaScript, so there's probably a better way to do it):
var data = [];
for (var i = 0; i < oldData.length; ++i) {
if (oldData[i] >= 2000) {
data[i] = 2000;
}
else data[i] = oldData[i];
}
Next thing, is o set manually the ticks you want and the tickLabels that correspond to it:
var ticks = [0,200,400,600,800,1000,1200,1400,1600,1800,2000];
var tickLabels = [0,200,400,600,800,1000,1200,1400,1600,1800,"> 2000"];
(notice that you can change to "1,200" and so on if you want the separator)
And instead of calling the d3.axisBottom(x) directly to your chart, I like to create a separate xAxis variable, and set the ticks and tickLabels to it:
var xAxis = d3.axisBottom(x)
.tickValues(ticks)
.tickFormat(function(d,i){ return tickLabels[i] });
Finally you call the xAxis on your chart:
chart.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
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'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
reference: https://github.com/mbostock/d3/wiki/Zoom-Behavior
//make zoom
var zoomFirst = d3.behavior.zoom()
.x(x)
.y(y1)
.scaleExtent([0, 3])
.size([w, h])
//.center([w/2+200, h/2-200])
.on("zoom", zoomedFirst);
function zoomedFirst() {
svgContainer.select(".x.axis").call(xAxis);
svgContainer.selectAll(".y.axis.axisLeft").call(yAxisLeft);
//set y2's scale manually
svgContainer.select(".price")
.attr("d", line1(priceData))
.attr("class", "price");
svgContainer.select(".difficulty")
.attr("d", line2(difficultyData))
.attr("class", "difficulty");
}
d3.behavior.zoom() supports autoscaling of x and y axes. However, I have to scale two y axes at the same time. When zoom() is triggered, I can get the current scale and translate info from d3.event.scale and d3.event.translate, but I cant figure out how to make appropriate scale for the second y axis(y2) with them.
I am also looking at https://github.com/mbostock/d3/wiki/Quantitative-Scales.
Since y1's range is automatically adjusted by zoom, if there is a way to get y1's current range, I can get its min and max and set y2's range based on them. However, the document doesn't specify a way to get range given a scale object.
Calling y1.range() (without any arguments) will return you the [min, max] of the scale.
From the docs:
If values is not specified, returns the scale's current output range.
Most accessor functions in D3 work like this, they return you (get) the value if you call them without any arguments and set the value if you call them with arguments and return the this object for easy chaining:
d3Object.propertyName = function (_) {
if (!arguments.length) return propertyName;
propertyName = _;
return this;
}
However, the zoom behaviour alters the domain and not the range of the scales.
From the docs:
Specifies an x-scale whose domain should be automatically adjusted when zooming.
Hence, you do do not need to get/set the range, but instead the domain of the scales y1 and y2: y2.domain(y1.domain()).
Since the zoom function already manages all the ratios, a more abbreviated answer would be:
var zoomFirst = d3.behavior.zoom()
.x(x)
.y(y1)
.scaleExtent([0, 3])
.size([w, h])
.on("zoom", function() {
zoomSecond.scale(zoom.scale());
zoomSecond.translate(zoom.translate());
// Update visual. Both y domains will now be updated
});
// Create copy for y2 scale
var zoomSecond = d3.behavior.zoom()
.x(x)
.y(y2) // <-- second scale
.scaleExtent([0, 3]) // <-- extent
This assumes you have called only zoomFirst to your visual.
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"));