I am creating a svg x-y-chart in d3.js. Is it possible to create ticks of different lengths depending on tickValue?
I have made my own tickFormat function myTickFormat and use that in .tickFormat([format]) and that works fine because [format] is expected to be a function. But it is not possible to do the same with .innerTickSize([size]), which expects a number.
E.g. if I want the tick at value 70 to be longer I want to do something like this:
var myTickSize = function(d) {
if (d === 70) { return 20;}
return 6;
};
But when I use myTickSize as argument to .innerTickSize():
var yScale = d3.scale.linear();
var yAxis = d3.svg.axis()
.scale(yScale).orient("left")
.innerTickSize(myTickSize);
I get an Error: Invalid value for attribute x2="NaN" error for each tick.
The tickSize function can only accept a number as argument, not a function, but there are other solutions.
The easiest approach? After the axis is drawn, select all the tick lines and resize them according to their data value. Just remember that you'll have to do this after every axis redraw, as well.
Example:
https://jsfiddle.net/zUj3E/1/
Key code:
d3.selectAll("g.y.axis g.tick line")
.attr("x2", function(d){
//d for the tick line is the value
//of that tick
//(a number between 0 and 1, in this case)
if ( (10*d)%2 ) //if it's an even multiple of 10%
return 10;
else
return 4;
});
Note that the tick marks at the max and min values are also drawn as part of the same <path> as the axis line, so shortening them doesn't have much effect. If you don't like that lack of control, declare the "outer" ticks to have zero length when you set up the axis. That turns off the corners of the path, but the outer ticks will still have lines that you can control the same as the other tick lines:
var axis = d3.svg.axis()
.tickSize(10,0)
Example: https://jsfiddle.net/zUj3E/2/
If that doesn't work, google for examples of major/minor tick patterns. Just make sure the example you're looking at uses d3 version 3: there were a few extra tick-related methods added in version 2 that are no longer supported. See this SO Q&A.
Variant on the answer that suits my requirements. Picked the values I just wanted tick marks for instead of ticks and value, added the "hide" class. But this can be used for any variation on the theme.
var gy = humiditySVG.append( "g" )
.attr( "class", "y axis" )
.attr( "transform", "translate(" + 154 + "," + 0 + ")" )
.call( yAxis );
humiditySVG.selectAll( "text" )
.attr( "class", function ( d ) {
if ( $.inArray(d, [0,50,100])==-1 ) {
return "hide";
}
return;
} );
humiditySVG.selectAll( "line" )
.attr( "x2", function ( d ) {
if ( $.inArray(d, [0,50,100])==-1 ) {
return 1;
} else {
return 3;
}
return;
} );
Related
I have a D3.js line chart that I want to update on user-input. One path is for ‘total price’, made up of a fixed price + a variable cost. I also show a ’fixed price’ line (not a path).
I have a slider to change the value of the fixed cost and then update the path and line.
The line takes the new inputted slider value and updates as expected. The path, however, starts being plotted with very negative y values and so doesn’t show on the chart.
Am I missing some logic to this?
If I hard-code a new value for ‘fixedCost’ the path updates as expected, but as soon as I substitute it for document.getElementById('fixed').value - it gives me a negative plot. The same problem occurs on first draw if I use the slider value.
I've successfully updated line charts in D3 before but that's usually loading new data set on a change event. I haven't encountered this problem with paths before. I'm using D3 V4. Below is the code for the slider and for the update function. Thanks
...javascript
var slider = d3.select("#chart").append("p").attr('id', 'slider')
.style('position', 'absolute')
.style('top', height + margin.top + 60 + 'px')
.style('left', margin.left + 'px')
.append("input")
.attr("type", "range")
.attr('id', 'fixed')
.attr("value", 408000)
.attr("min", 0)
.attr("max", 1000000)
.style("width", sliderWidth)
.on("input", updateFixed);
// set starting parameters
// var fixedCost = document.getElementById('fixed').value; // doesn't behave as expected when plotting path.
var fixedCost = 408000;
var valuelineTotCost = d3.line()
.x(function(d) { return x(d.subs); })
.y(function(d) { return y(d.variCost + fixedCost); });
function updateFixed() {
var thisValue = document.getElementById('fixed').value;
d3.select('#sliderText')
.text("Fixed Costs: " + format(thisValue) ); // displays as expected
console.log(thisValue); // returns as expected
svg.select('#fixedCostLine')
.attr("x1", 0)
.attr("x2", width)
.attr("y1", y(thisValue))
.attr("y2", y(thisValue)); // this line updates as expected
// var fixedCost = document.getElementById('fixed').value; // tried this instead of using thisValue but still not behaving as expected
// var fixedCost = thisValue; // also not behaving as expected
var fixedCost = 508000; // behaves as expected
// adjust totCost path
var valuelineTotCost = d3.line()
.x(function(d) { return x(d.subs); })
.y(function(d) { return y(d.variCost + fixedCost); });
svg.select("#totalPath")
.style("stroke", "purple")
.attr("d", valuelineTotCost);
};
...
Here are the original path co-ordinates generated by d3.line followed by the negative y plots given when adjusted (even minutely) using the slider value.
Lastly, using a hard-coded value for the update, switching from the starting point of 408000 to 508000 - gave the third set of plots.
The path generator was reading the value from the input as a string.
thisValue = parseInt(thisValue);
solved the problem.
I've been playing around with this example here for a little while. What I'm trying to do is highlight a single node/circle in the plot (by making it larger with a border; later I want to add text or a letter inside it too).
Currently, I've made the circle for Bhutan larger in the plot like the following:
.attr("r",
function(d){return ( d.countryName === "Bhutan" ? r + 4 : r);})
.attr("stroke", function(d){if (d.countryName==="Bhutan"){return "black"}})
However, it overlaps with the other circles. What would be the best approach to avoid these collisions/overlaps? Thanks in advance.
Link to Plunkr - https://plnkr.co/edit/rG6X07Kzkg9LeVVuL0PH?p=preview
I tried the following to add a letter inside the bhutan circle
//find bhutan circle and add a "B" to it
countriesCircles
.data(data)
.enter().append("text")
.filter(function(d) { return d.countryName === "Bhutan"; })
.text("B");
Updated Plunkr - https://plnkr.co/edit/Bza5AMxqUr2HW9CYdpC6?p=preview
This is a slightly different problem than in this question here: How to change the size of dots in beeswarm plots in D3.js
You have a few options that I can think of:
Set the forceCollide to be your largest possible radius * 1.33, e.g. (r + 4) * 1.33. This will prevent overlapping, but spread things out a lot and doesn't look that great.
Add the radius property to each entry in your array and make the collide work based off that, which will look a bit better but not perform as awesomely for large sets.
Here's an example of how to do that:
...
d3.csv("co2bee.csv", function(d) {
if (d.countryName === "Bhutan") {
d.r = r + 4;
} else {
d.r = r;
}
return d;
}, function(error, data) {
if (error) throw error;
var dataSet = data;
...
var simulation = d3.forceSimulation(dataSet)
...
.force("collide", d3.forceCollide(function(d) { return d.r * 1.33; }))
...
countriesCircles.enter()
.append("circle")
.attr("class", "countries")
.attr("cx", 0)
.attr("cy", (h / 2)-padding[2]/2)
.attr("r", function(d){ return d.r; })
....
Use the row function in d3.csv to add a property to each member of the array called r, and check the country name to determine which one gets the larger value. Then use that value wherever you need to mess with the radius.
I guess it would've been possible to check the country name everywhere the radius was impacted (e.g. .force("collide", d3.forceCollide(function(d) { return d.countryName === "Bhutan" ? (r + 4) * 1.33 : r * 1.33; }), etc.). This feels a bit cleaner to me, but it might be cleaner still by abstracting out the radius from the data entries themselves...
Forked your plunk here: https://plnkr.co/edit/Tet1DVvHtC7mHz91eAYW?p=preview
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'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"));