D3 Logarithmic Tick Labels as Powers of 10 - javascript

Using D3 I want to create an X Axis that looks like:
I've worked out how to do the axis and ticks, but not the labels using the following:
var svgWidth = 500;
var svgHeight = 500;
var svgAxisPadding = 20;
var xScale = d3.scale.log()
.domain([Math.pow(10, 5), Math.pow(10, 7)])
.range([svgAxisPadding, svgWidth - svgAxisPadding]);
var xAxis = d3.svg.axis()
.scale(xScale)
.orient('bottom')
.ticks(0, "e");
var svg = d3.select('#diagram')
.append('svg')
.attr('width', svgWidth)
.attr('height', svgHeight);
svg.append('g')
.attr("class", "axis")
.call(xAxis);
And here's a jsFiddle with the complete code.

You could use unicode:
var superscript = "⁰¹²³⁴⁵⁶⁷⁸⁹",
formatPower = function(d) { return (d + "").split("").map(function(c) { return superscript[c]; }).join(""); },
formatTick = function(d) { return 10 + formatPower(Math.round(Math.log(d) / Math.LN10)); };
For example, formatTick(1e5) returns "10⁵". Example at bl.ocks.org/6738109:
The downside of this approach is that the vertical alignment of the superscript numerals seems inconsistent. So using post-selection (say, selecting the text elements and adding a tspan element for the superscript to each) might be better. Another example at bl.ocks.org/6738229:

There's a tickFormat function available on the axis. Unfortunately, it expects a String as a return value and plops that on the axis. This would be great if you wanted to display 10^6, but not as helpful when you want to use the superscript notation.
A workaround is to create 2 axes: one for displaying the 10 and another for displaying the exponent. Here's an example:
var svgWidth = 500;
var svgHeight = 500;
var svgAxisPadding = 20;
var xScale = d3.scale.log()
.domain([Math.pow(10, 5), Math.pow(10, 7)])
.range([svgAxisPadding, svgWidth - svgAxisPadding]);
var xAxis = d3.svg.axis()
.scale(xScale)
.orient('bottom')
.ticks(0, "e")
.tickFormat(function (d) {
var log = Math.log(d) / Math.LN10;
return Math.abs(Math.round(log) - log) < 1e-6 ? 10 : '';
});
var xAxis2 = d3.svg.axis()
.scale(xScale)
.orient('bottom')
.ticks(0, "e")
.tickFormat(function (d) {
var log = Math.log(d) / Math.LN10;
return Math.abs(Math.round(log) - log) < 1e-6 ? Math.round(log) : '';
});
var svg = d3.select('#diagram')
.append('svg')
.attr('width', svgWidth)
.attr('height', svgHeight);
svg.append('g')
.attr("class", "axis")
.call(xAxis);
svg.append('g')
.attr("class", "axis")
.attr("transform", "translate(12, -5)") //shifted up and to the right
.style("font-size", "12px")
.call(xAxis2);
It's not necessarily the most elegant solution, but it works.

Related

yScale not acting consistently d3.js

I'm drawing a bar chart with axes, and yScale is behaving differently on my yAxis than on my appended bars.
I set my yScale range to start at (h - yPadding) to leave extra room at the bottom for xAxis labels.
var yScale = d3.scale.linear()
.domain([0, d3.max(val)])
.range([h - yPadding, 0]);
-- The range is inverted, otherwise my yAxis labels are upside down.
When I call the yAxis using yScale, it obeys the starting point of (h - yPadding) and leaves room at the bottom.
But all the "rects" I'm appending to the chart, start at h, instead of (h - yPadding) even though I'm calling yScale on these "rects" just like on yAxis.
If I change the range to [h, 0] instead of [h - yPadding, 0], only the yAxis reacts to the change, and the bars still start at h.
Why are the bars ignoring the yScale?
<script type="text/javascript">
var xhr = new XMLHttpRequest();
function makeRequest(){
xhr.open("GET", "https://api.coinmarketcap.com/v1/ticker/", true);
xhr.send();
xhr.onreadystatechange = processRequest;
}
function processRequest(){
console.log("testing, state: ", xhr.readyState)
if(xhr.readyState == 4 && xhr.status == 200){
dataset = [];
for(var i = 0; i < 10; i++){
addingId = JSON.parse(xhr.responseText)[i];
addingId.id = i;
dataset.push(addingId);
}
console.log("this is dataset: ", dataset);
makeChart();
}
}
function makeChart(){
var w = 1000;
var h = 600;
var padding = 40;
var yPadding = 80;
var val = [];
dataset.forEach(function(ele){
val.push(parseInt(ele.market_cap_usd));
})
var max = d3.max(val)
var xAxisNames = []
dataset.forEach(function(ele){ xAxisNames.push(ele.name); })
// console.log(">>>>>>>>", xAxisNames)
var xScale = d3.scale.ordinal()
.domain(d3.range(dataset.length))
.rangeRoundBands([padding, w - padding], 0.05)
var yScale = d3.scale.linear()
.domain([0, d3.max(val)])
.range([h - yPadding, 0]);
var yAxis = d3.svg.axis()
.scale(yScale)
.orient("left")
.tickFormat(function(d){
if(d > 0){ return d / 1000000000 + " b"; }
return "";
})
var xAxis = d3.svg.axis()
.scale(xScale)
.orient("bottom")
.tickFormat(function(d, i){
return xAxisNames[i]
})
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("x", function(d, i){
return xScale(i);
})
.attr("y", function(d){
return yScale(d.market_cap_usd);
})
.attr("width", xScale.rangeBand())
.attr("height", function(d, i){
return h - yScale(d.market_cap_usd)
})
svg.append("g")
.attr("class", "y axis")
.attr("transform", "translate(" + padding + ", 0)")
.call(yAxis);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0, " + (h - yPadding) + ")")
.call(xAxis)
.selectAll("text")
.attr("y", 15)
.attr("font-size", 12)
.attr("x", xScale.rangeBand() / 2)
.attr("transform", "rotate(45)")
}
makeRequest();
</script>
A scale just maps an input domain to an output range, nothing more. You have to set the positions and the dimensions of the SVG elements accordingly. Let's see:
Right now, given your scale, when you pass it the minimum value in your domain it will return:
h - yPadding
You want such bars having a height of zero pixels, of course. To get that zero the equation is simple, you have to subtract from that value:
(h - yPadding) - yScale(minimumDomainValue)
That will give you zero for the minimum value in the domain.
Therefore, this should be the height of the rectangles:
.attr("height", function(d, i){
return (h - yPadding) - yScale(d.market_cap_usd)
})
PS: by the way, in D3, one of the few situations where a scale determines the dimensions​ of a SVG element is the path/lines created by an axis generator. That's why you're seeing a different behaviour in your axis.

Bar chart output isn't what's expected

I am making a bar chart with D3. Most of the work is done, but the output is not behaving as I expect it to, and I can't figure the problem out.
The sample data is this, which is a JSON file comprising an array of figures for Gross Domestic Product in the US.
My bar chart is showing the curve I expect, although the figures start just over 2000, whereas in the data, they start at just over 200. I have attempted to change around the values but each time I tinker with the y positioning and height, I get unexpected results.
JS:
var fccDataUrl = 'https://raw.githubusercontent.com/FreeCodeCamp/ProjectReferenceData/master/GDP-data.json';
var dataset;
var fccData;
$.getJSON(fccDataUrl, (myData) => {
dataset = myData.data;
fccData = myData;
console.log('dataset', dataset)
console.log('fccData', fccData)
var w = '800'
var h = '500'
var padding = 50;
var svg = d3.select('body')
.append('svg')
.attr('width', w)
.attr('height', h)
var xScale = d3.scale.linear()
.domain([1947, 2015])
.range([padding, w - padding])
var yScale = d3.scale.linear()
.domain([0, d3.max(dataset, d => d[1])])
.range([h - padding, padding])
var xAxis = d3.svg.axis()
.scale(xScale)
.orient('bottom')
.ticks(10)
var yAxis = d3.svg.axis()
.scale(yScale)
.orient('left')
.ticks(10)
svg.selectAll('rect')
.data(dataset)
.enter()
.append('rect')
.classed('bars', true)
.attr('x', (d, i) => i * (w / dataset.length) + 50)
.attr('y', d => yScale(d[1]) - 50)
.style('width', '4px')
.style('height', d => h - yScale(d[1]))
svg.append('g')
.attr('class', 'axis')
.attr('transform', `translate(0, ${h - padding})`)
.call(xAxis)
svg.append('g')
.attr('class', 'axis')
.attr('transform', `translate(${padding}, 0)`)
.call(yAxis)
});
Here is a codepen of my work so far.
There is some mix-up between h, and the chart height. Here is an updated codepen with the padding, h, and chartHeight defined separately.
var fccDataUrl = 'https://raw.githubusercontent.com/FreeCodeCamp/ProjectReferenceData/master/GDP-data.json';
var dataset;
var fccData;
$.getJSON(fccDataUrl, (myData) => {
dataset = myData.data;
fccData = myData;
console.log('dataset', dataset)
console.log('fccData', fccData)
var w = '800'
var h = '500'
var padding = {top: 50, bottom: 50, right: 50, left: 50};
var chartWidth = w - padding.left - padding.right;
var chartHeight = h - padding.top - padding.bottom;
var svg = d3.select('body')
.append('svg')
.attr('width', w)
.attr('height', h)
.append("g") // apply the transform to the parent elem instead of individually.
.attr("transform", `translate(${padding.left}, ${padding.top})`)
var xScale = d3.scale.linear()
.domain([1947, 2015])
.range([0, chartWidth]) // no need to account for padding anymore
var yScale = d3.scale.linear()
.domain([0, d3.max(dataset, d => d[1])])
.range([chartHeight, 0]) // no need to account for padding anymore
var xAxis = d3.svg.axis()
.scale(xScale)
.orient('bottom')
.ticks(10)
var yAxis = d3.svg.axis()
.scale(yScale)
.orient('left')
.ticks(10)
svg.selectAll('rect')
.data(dataset)
.enter()
.append('rect')
.classed('bars', true)
.attr('x', (d, i) => i * (w / dataset.length)) // no need to account for padding anymore
.attr('y', d => yScale(d[1])) // no need to account for padding anymore
.style('width', '4px')
.style('height', d => chartHeight - yScale(d[1]))
svg.append('g')
.attr('class', 'axis')
.attr('transform', `translate(0, ${chartHeight})`) // no need to account for padding anymore
.call(xAxis)
svg.append('g')
.attr('class', 'axis')
//.attr('transform', `translate(${padding}, 0)`) // no need to account for padding anymore
.call(yAxis)
});

how to draw a line between two axes with a data? (d3.js)

I am trying to create a parallel coordinates with my own code, so that i can learn d3.js very well. Now i am stuck in a situation. I have two axis with some data in it
and i want to connect the data with a line. I tried of getting the position of the two data in the axes but its not working for me and its got complicated
Is there any way to connect the axes like this?
The jsfiddle link is commented below. Please find it
Thanks for the help
Appending an svg:line between those ticks is the way to go but the hard part is finding the proper positioning within the overall SVG document. Since things are being transitioned twice (once for the axis g and once for the tick g), you have two options, sum up all the positions by using d3.tranform on the elements, or use something like getBoundingClientRect on the node.
In the below code I've chosen the later. This quick function will take the text value of any two ticks and draw a line. Note, those text values have to be unique:
function addLine(t1, t2){
var ticks = {};
d3.selectAll('.tick text').each(function(d) {
ticks[d3.select(this).text()] = this;
});
var pos1 = ticks[t1].getBoundingClientRect();
var pos2 = ticks[t2].getBoundingClientRect();
svg.append('line')
.attr('x1', pos1.left)
.attr('y1', pos1.top + 5)
.attr('x2', pos2.left - 5)
.attr('y2', pos2.top + 5)
.style('stroke','black');
}
addLine('a', 'ab');
addLine('a', 'bb');
Full working example:
var w = 200;
var h = 400;
var padding = 100;
var x = ["a","b"];
var z = ["aa","ab","ba","bb"];
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
for(var i=1;i<3;i++){
var yScale = d3.scale.linear()
.domain([0, i === 1 ? x.length : z.length])
.range([h - padding, padding]);
//Define Y axis
var yAxis = d3.svg.axis()
.scale(yScale)
.ticks(i === 1 ? x.length : z.length)
.orient("left")
.tickSize(1)
.tickFormat(function(d){
return i === 1 ? x[d] : z[d];
})
// .style("text-anchor", "middle");
//Create SVG element
svg.append("g")
.attr("class", "axis" + i)
.attr("transform", "translate("+(i*padding)+",0)")
.call(yAxis)
.attr("fill","red");
}
function addLine(t1, t2){
var ticks = {};
d3.selectAll('.tick text').each(function(d) {
ticks[d3.select(this).text()] = this;
});
var pos1 = ticks[t1].getBoundingClientRect();
var pos2 = ticks[t2].getBoundingClientRect();
svg.append('line')
.attr('x1', pos1.left)
.attr('y1', pos1.top + 5)
.attr('x2', pos2.left - 5)
.attr('y2', pos2.top + 5)
.style('stroke','black');
}
addLine('a', 'ab');
addLine('a', 'bb');
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

D3.js - how to draw grid lines for specific axis ticks

I am trying to draw some line charts using D3.js. I found some codes like http://jsfiddle.net/BDWFW/.
var w = 760;
var h = 400;
var pad = 50;
var d0 = new Date("Jan 29 2011 UTC");
var d1 = new Date("March 15 2011 UTC");
var x = d3.time.scale() .domain([d0, d1]).range([0,w]);
var y = d3.scale.linear().domain([0, 1]) .range([h,0]);
var svg = d3.select("body")
.append("svg:svg")
.attr("height", h + pad)
.attr("width", w + pad)
var vis = svg.append("svg:g")
.attr("transform", "translate(40,20)")
var rules = vis.append("svg:g").classed("rules", true)
function make_x_axis() {
return d3.svg.axis()
.scale(x)
.orient("bottom")
.ticks(8)
}
function make_y_axis() {
return d3.svg.axis()
.scale(y)
.orient("left")
.ticks(10)
}
rules.append("svg:g").classed("grid x_grid", true)
.attr("transform", "translate(0,"+h+")")
.call(make_x_axis()
.tickSize(-h,0,0)
.tickFormat("")
)
rules.append("svg:g").classed("grid y_grid", true)
.call(make_y_axis()
.tickSize(-w,0,0)
.tickFormat("")
)
rules.append("svg:g").classed("labels x_labels", true)
.attr("transform", "translate(0,"+h+")")
.call(make_x_axis()
.tickSize(5)
// .tickFormat(d3.time.format("%Y/%m"))
)
rules.append("svg:g").classed("labels y_labels", true)
.call(make_y_axis()
.tickSubdivide(1)
.tickSize(10, 5, 0)
)
As we can see in the result, all grid lines are connected to every ticks. Instead, I would like to choose specific ticks and draw grid lines only for those ticks. Unfortunately, I am not sure how I can do it with D3.js. For example, in this example, how can we make gridlines for Feb06, Feb20, and Mar06?
Sometimes, I really don't understand D3.js. Look at this gridline drawing part:
rules.append("svg:g").classed("grid y_grid", true)
.call(make_y_axis()
.tickSize(-w,0,0)
.tickFormat("")
)
I am really not sure about how this code draws a gridline. Among the chained function, I couldn't find which one doing that. Some novice users like me feel like everything is in a Blackbox.

d3.js how to place custom labels on horizontal log scaled axis

I am drawing a chart with d3.js using d3.scale.log for the x axis in combination with a custom labels. Unfortunately the labels run into each other ... any hints on how to make this work?
var width = 400;
var x = d3.scale.log().domain([1, 1000]).range([0, width]);
var formatSi = d3.format(".4s");
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom")
.tickFormat(function(d, i) {
return formatSi(d) + 'Hz'
});
var svg = d3.select("#chart")
.append("svg").attr("width", width)
.attr("height", 200)
.append("g").attr("transform", "translate(20,20)");
([1, 3, 6, 9]).forEach(function(d) {
x.domain([1, Math.pow(10, d)]);
svg.append("g").attr("class", "axis x")
.attr("transform", "translate(0," + d * 2 + "0)")
.call(xAxis);
});​
A working example of this code is on jsfiddle
figured it out. Trick is to not use .tickFormat to supply the formatting function, but rather the .ticks method which in turn will apply the supplied formatting function to the .tickFormat method of the scale.
var width = 400;
var x = d3.scale.log().domain([1, 1000]).range([0, width]);
var formatSi = d3.format(".4s");
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom")
​   .ticks(5,function(d, i) {
        return formatSi(d) + 'Hz';
    });
var svg = d3.select("#chart")
.append("svg").attr("width", width)
.attr("height", 200)
.append("g").attr("transform", "translate(20,20)");
([1, 3, 6, 9]).forEach(function(d) {
x.domain([1, Math.pow(10, d)]);
svg.append("g").attr("class", "axis x")
.attr("transform", "translate(0," + d * 2 + "0)")
.call(xAxis);
});
The result is still not entirely satisfying as the system seems to have trouble placeing labels when the ticks are close together ...

Categories