Creating axis with custom (range) values - javascript

I would like to add a xAxis with custom values to my density plot (see Screenshot).
First density "violin" stands for "0-50", second for "51-100", third for "101-150" ... and seventh density for "301-350" passengers. It should look similar to what is shown in the following edited screenshot.
As I am a rookie in d3 and javascript I would appreciate detailed answers.
I am working with d3.js (v4).
Here is my current code:
function addViolin(svg, dataOfBin, heightPlot, widthPlot, domain, imposeMax, violinColor){
var data = d3.histogram()
.thresholds(resolution)
(dataOfBin);
var y = d3.scaleLinear()
.range([widthPlot/2, 0])
.domain([0, Math.max(imposeMax, d3.max(data, function(d) { return d.length; }))]);
var x = d3.scaleLinear()
.range([heightPlot, 0])
.domain(domain)
.nice();
var area = d3.area()
.curve(d3.curveCardinal)
.x(function(d) {
if(interpolation=="step-before")
return x(d.x1/2)
return x(d.x);
})
.y0(widthPlot/2)
.y1(function(d) { return y(d.length); });
var line=d3.line()
.curve(d3.curveCardinal)
.x(function(d) {
if(interpolation=="step-before")
return x(d.x1/2)
return x(d.x);
})
.y(function(d) { return y(d.length); });
var gPlus=svg.append("g")
var gMinus=svg.append("g")
gPlus.append("path")
.datum(data)
.attr("class", "area")
.attr("d", area)
.style("fill", violinColor);
gPlus.append("path")
.datum(data)
.attr("class", "violin")
.attr("d", line)
.style("stroke", violinColor);
gMinus.append("path")
.datum(data)
.attr("class", "area")
.attr("d", area)
.style("fill", violinColor);
gMinus.append("path")
.datum(data)
.attr("class", "violin")
.attr("d", line)
.style("stroke", violinColor);
gPlus.attr("transform", "rotate(90,0,0) translate(0,-"+widthPlot+")");//translate(0,-200)");
gMinus.attr("transform", "rotate(90,0,0) scale(1,-1)");
}
function addBoxPlot(svg, dataOfBin, heightPlot, widthPlot, domain, boxPlotWidth, boxColor, boxInsideColor){
}
var marginPlot={top:10, bottom:0, left:30, right:10};
var widthPlot=400;
var heightPlot=220;
var boxWidth=44;
var boxSpacing=10;
var resolution=1000;
var d3ObjId="svgElement1";
var interpolation='step-before';
function showView3(data){
var regionChart = document.getElementById("view3Diagram");
if(regionChart != null){
regionChart.innerHTML = "";
}
var domain=[0, 370];
var y = d3.scaleLinear()
.range([heightPlot-marginPlot.bottom, marginPlot.top])
.domain(domain);
var yAxis = d3.axisLeft()
.scale(y)
.ticks(5)
.tickSize(5,0,5);
var svg = d3.select("div#view3Diagram")
.append("div")
.classed("svg-container2", true)
.append("svg")
.attr("preserveAspectRatio", "xMinYMin meet")
.attr("viewBox", "0 0 430 430")
.classed("svg-content-responsive", true);
svg.append("line")
.attr("class", "boxplot")
.attr("x1", marginPlot.left)
.attr("x2", widthPlot-marginPlot.right)
.attr("y1", y(0))
.attr("y2", y(0));
for(var i=0; i<data.length; i++){
data[i].Haltezeiten=data[i].Haltezeiten.sort(d3.ascending)
var g=svg.append("g").attr("transform", "translate("+(i*(boxWidth+boxSpacing)+marginPlot.left)+",0)");
addViolin(g, data[i].Haltezeiten, heightPlot, boxWidth, domain, 0.25, "#424242");
addBoxPlot(g, data[i].Haltezeiten, heightPlot, boxWidth, domain, .15, "black", "white");
}
svg.append("g")
.attr('class', 'axis')
.attr("transform", "translate("+marginPlot.left+",0)")
.call(yAxis);
}

As you requested, I'll write a detailed explanation in this answer (however, this code doesn't seem the work of a rookie...).
As we can see, you use a for loop...
for(var i=0; i<data.length; i++){
... to create the <g> elements and, inside each one of them, the violin plots.
So, the most important thing here is data.length. We'll use that to set the domain or our point scale, which will be fundamental for customizing the axis' ticks later. Follow me:
If you pass d3.range(data.length) as the domain of the scale, you'll have an array of numbers with the same length of data.length. For instance, if data.length is 7, this is the result of d3.range(data.length):
[0, 1, 2, 3, 4, 5, 6]
Then, with this domain, we can create a customization in the axis using tickFormat. For that, I'm going to set a variable named steps, which says what's the gap between the values in the first tick. In your example:
var steps = 50;
With the domain and the step set, this is the math in the tickFormat:
.tickFormat(function(d, i) {
return i ? (d * steps) + 1 + " - " + (d * steps + steps)
: (d * steps) + " - " + (d * steps + steps);
});
In case you don't know, this is a ternary operator. It's basically:
condition ? expr1 : expr2
If condition is true, expr1 is executed, otherwise expr2 is executed.
Make sure that you pass the same range to the scale, and that you translate it accordingly.
Here is the running demo:
var svg = d3.select("svg");
var steps = 50;
var data = ["foo", "foo", "foo", "foo", "foo", "foo", "foo"];
var scale = d3.scalePoint()
.range([20, 470])
.domain(d3.range(data.length));
var axis = d3.axisBottom(scale)
.tickFormat(function(d, i) {
return i ? (d * steps) + 1 + " - " + (d * steps + steps) : (d * steps) + " - " + (d * steps + steps);
});
var gX = svg.append("g")
.attr("transform", "translate(0,50)")
.call(axis)
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="500" height="500"></svg>

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.

d3.js x-coord displaying incorrectly iOS

Using d3.js the x coordinates of the graph are displaying at x=0 or on the y axis. The x axis represents a date and time and the y axis is the temperature. But this is only on an ipad or iphone. On my own machine, Linux, it displays correctly.
The graphs and all file can be seen at,
http://shanespi.no-ip.biz
The ipad/iphone display
While the correct graph is,
Here is the javascript,
var xScale = d3.scaleTime()
.domain([new Date(datahourly[0].date), d3.max(datahourly, function(d) {return new Date(d.date)})])
.range([0, (w-2*padding)]); // max x screen space is width - twice padding
var yScale = d3.scaleLinear()
.domain([0, d3.max(datahourly, function(d) {return d.temp})])
.range([(h-2*padding), 0]); // max y screen space is height - twice padding
var xAxis = d3.svg.axis(xScale) // d3 v.4
.ticks(9) // specify the number of ticks
/*.ticks(d3.time.days, 1) */
.tickFormat(d3.time.format('%H:00'))
.scale(xScale)
.orient("bottom");
var yAxis = d3.svg.axis(yScale)
.ticks(7)
.scale(yScale)
.orient("left");
var svg = d3.select('#hourly-readings')
.append('svg') // create an <svg> element
.attr('id', 'svgDaily')
.attr('width', w) // set its dimensions
.attr('height', h);
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(" + (2*padding - 15) + "," + (padding - 15) + ")")
.call(yAxis);
svg.append('g') // create a <g> element
.attr('class', 'axis') // specify classes
.attr("transform", "translate(" + (2*padding - 15) + "," + (h - padding - 15) + ")")
.call(xAxis); // let the axis do its thing
var lineFunctionStart = d3.svg.line()
.x(function(d) {return xScale(new Date(d.date)); })
.y(h - 2*padding - 5)
.interpolate("cardinal");
var lineFunction = d3.svg.line()
.x(function(d) {return xScale(new Date(d.date)); })
.y(function(d) {return yScale(d.temp); })
.interpolate("cardinal");
svg.append("path")
.attr('d', lineFunctionStart(datahourly))
.attr('stroke', "grey")
.attr('stroke-width', 1)
.style('fill', "white")
.attr("transform","translate(" + (2*padding - 13) + "," + (padding - 10) + ")")
.transition()
.duration(3000)
.attr('d', lineFunction(datahourly));
//var svg = d3.select('svg');
var svg = d3.select('#svgDaily');
svg.append("text") // text label for the x axis
.attr("x", 310)
.attr("y", h)
.style("font-size", "12")
.style("text-anchor", "middle")
.text("Time (1 hr. intervals)");
svg.append("text") // text label for the x axis
.attr('transform', 'rotate(-90)')
.attr("x", -85) // Because rotate is first x and y coordinates are transaposed
.attr("y", padding-17)
.style("font-size","10")
.style("text-anchor", "middle")
.text("Temp. Celcius");
var rects = svg.selectAll('circle')
.data(datahourly);
var newRects = rects.enter();
newRects.append('circle')
.attr('cx', function(d, i) { return (Math.random() * (w - 2*padding)) })
//.attr('cx', function(d, i) {
//return (5 + xScale(new Date(d.date)));
//})
.attr('cy', (h - (2*padding)))
.attr('r', 5)
.style('fill', "lightblue")
.attr("transform","translate(" + (2*padding - 18) + "," + (padding - 20) + ")")
.transition()
.duration(3000)
.delay(function(d, i) {return i * 300})
.attr('cx', function(d, i) {
return (5 + xScale(new Date(d.date)));
})
.attr('cy', function(d, i) {
return 10 + yScale(d.temp);
});
Here is the 'datahourly' data,
[
{
"date":"2016-12-14 22:01:01.799830",
"temp":"24.04"
},
{
"date":"2016-12-15 00:01:02.362875",
"temp":"23.03"
},
......................
{
"date":"2016-12-15 21:01:01.868593",
"temp":"21.93"
},
{
"date":"2016-12-15 22:01:02.278817",
"temp":"15.9"
},
{
"date":"2016-12-15 23:01:01.963714",
"temp":"21.63"
}
]
I am using Chrome on Linux and Safari on the ipad and iphone. But I did install chrome on the iphone and the graph is still incorrect.
Are there svg issues with iOS?
EDIT:
The main issue was that the time data was not parsed correctly,
This is the correct solution,
var data = [];
$.getJSON("data/data.json",
function(info){
data = info[0].fiveMinReadings;
//console.log(data);
var parseTime = d3.timeParse("%Y-%m-%d %H:%M:%S.%L");
data.forEach(function(d) {
d.date = d.date.slice(0,-3);// remove microseconds
d.date = parseTime(d.date);
d.temp = +d.temp;
});
// Beginning of graph for 5 minute readings
var padding = 25;
var w = 600;
var h = 300;
var xScale = d3.scaleTime()
.domain(d3.extent(data, function(d) { return d.date; }))
.range([0, (w-2*padding)]); // max x screen space is width - twice padding
var yScale = d3.scaleLinear()
.domain([0,d3.max(data, function(d) {return d.temp})])
.range([(h-2*padding), 0]); // max y screen space is height - twice padding
var xAxis = d3.axisBottom(xScale) // d3 v.4
.tickFormat(d3.timeFormat('%H:%M '))
.scale(xScale);
var yAxis = d3.axisLeft(yScale)
.scale(yScale);
var svg = d3.select('#five-min-readings')
.append('svg') // create an <svg> element
.attr('id','svgHourly')
.attr("align","center")
.attr('width', w) // set its dimensions
.attr('height', h);
var valueline = d3.line()
.curve(d3.curveCardinal)
.x(function(d) { return xScale(d.date); })
.y(h - 2*padding - 4);
var valueline2 = d3.line()
.curve(d3.curveCardinal)
.x(function(d) { return xScale(d.date); })
.y(function(d) {return yScale(d.temp); });
svg.append("text") // text label for the x axis
.attr("x", 310)
.attr("y", h)
.style("font-size", "12")
.style("text-anchor", "middle")
.text("Time (5 min. intervals)");
svg.append("text") // text label for the x axis
.attr('transform', 'rotate(-90)')
.attr("x", -85) // Because rotate is first, x and y coordinates are transaposed
.attr("y", padding-17)
.style("font-size","10")
.style("text-anchor", "middle")
.text("Temp. Celcius");
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(" + (2*padding-15) + "," + (padding-15) + ")")
.call(yAxis);
svg.append('g') // create a <g> element
.attr('class', 'axis') // specify class
.attr("transform", "translate(" + (2*padding-15) + "," + (h - padding - 15) + ")")
.call(xAxis); // let the axis do its thing
svg.append('path')
.data([data])
.attr("class","line")
.attr('d', valueline)
.attr('stroke', "grey")
.attr('stroke-width', 1)
.style('fill', "white")
.attr("transform","translate(" + (2*padding - 13) + "," + (padding -10) + ")")
.transition()
.duration(3000)
.attr('d', valueline2 );
var svg = d3.select('#svgHourly');
var rects = svg.selectAll('circle')
.data(data);
var newRects = rects.enter();
newRects.append('circle')
.attr('cx', function(d, i) { return (Math.random() * (w - 2*padding)) })
.attr('cy', h - 2*padding)
.attr('r', 5)
.attr("id", function(d,i){return "circle" + i})
.style('fill', "lightblue")
.attr("transform","translate(" + (2*padding - 18) + "," + (padding - 20) + ")")
.transition()
.duration(3000)
.delay(function(d, i) {return i * 300})
.attr('cx', function(d, i) { return (5 + xScale(d.date)); })
.attr('cy', function(d, i) { return 10 + yScale(d.temp); });
}); // closes getJSON()
You can clearly see that your circles are getting the correct y ("cy") value, the error lies in the x ("cx") value.
The problem seems to be the use of new Date() in Safari with this pattern: yyyy-MM-dd.
In your code, given your data structure, you'll end up having something like this in the line generator:
.x(function(d) {
return xScale(new Date("2016-12-15 23:01:01.963714"));
})//the first date in your data ---^
And the same for your circles:
.attr('cx', function(d, i) {
return (5 + xScale(new Date("2016-12-15 23:01:01.963714")));
})
Apparently, this is supported by Chrome and Firefox, but not by Safari. Curiously, the pattern (yyyy-MM-dd) is included in the ECMA standard, so this is probably a Safari specific issue.
According to this answer, it will work if you include a T (I didn't test it):
.x(function(d) {
return xScale(new Date("2016-12-15T23:01:01.963714"));
})
Alternatively, remove the new Date() and parse the dates using D3 (d3.timeParse() in d3 v4.x and format.parse() in d3 v3.x).
EDIT: Summarising, you have two possible solutions:
Solution 1: Remove all new Date functions (both on the line generator and in all scales that use it) and parse the date using d3. You said that you're using d3 v3.x, but your code is using d3 v4.x instead. Nevertheless, here is how to do it using d3 v3.x:
var date = "2016-12-14 22:01:01.799830".slice(0,-3);
var format = d3.time.format("%Y-%m-%d %H:%M:%S.%L");
var myDate = format.parse(date);
console.log(myDate);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
Note: I'm removing the last 3 digits because d3 can parse onli miliseconds, not microseconds.
Solution 2: Keep your new Date functions, but add a T as already discussed above:
var date = "2016-12-14 22:01:01.799830".replace(/\s/, 'T');
console.log(date);
console.log(new Date(date));
Seems there's a issue with compatibility between dc.js 1.7.5 the stable version and d3.js v4. dc.js compatibility with v4 of d3.js and my new post on this https://stackoverflow.com/questions/41389307/dc-js-1-7-5-not-compatible-with-d3-js-v4
Wow, this method works commonly in both android and iOS, saved my hours of browsing about this issue
var commonDate = item.created_at.replace(/\s/, "T");
var date = new Date(commonDate);
Long story short: I have used d3 in the ionic angular version and had the same issue in iOS alone

d3js updates only once

I have a visualization task that I need to make it done with d3.js. Here's my code.
var w = 700;
var h = 500;
var offset = 100;
var padding = 20;
var colors = d3.scale.category10();
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
var texts = function(ds,ds2){
var stack = d3.layout.stack();
stack_data = stack(ds);
var xScale = d3.scale.ordinal()
.domain(d3.range(ds[0].length))
.rangeRoundBands([0, w-offset], 0.50);
var yScale = d3.scale.linear()
.domain([0,
d3.max(stack_data, function(d) {
return d3.max(d, function(d) {
return d.y0 + d.y -20;
});
})
])
.range([padding, h-50]);
var xAxis = d3.svg.axis()
.scale(xScale)
.orient("bottom")
.ticks(ds[0].length);
gs = svg.selectAll("g").data(stack_data);
for (var i5 = 0; i5 < ds.length; i5++) {
gs.enter()
.append("g")
.attr("class", "stacked_bars")
.attr("fill", function(d, i) {
return colors(i);
});
asd = gs.selectAll("rect").data(function(d) { return d; });
asd.enter().append("rect");
asd.transition()
.duration(1000)
.attr("x", function(d, i) {
return xScale(i);
})
.attr("y", function(d) {
return h - yScale(d.y0) - yScale(d.y);
})
.attr("height", function(d) {
return yScale(d.y);
})
.attr("width", xScale.rangeBand())
.attr("class", "rectbar");
};
gs.append("g") // add a group element to the svg
.attr("class", "axis") //Assign class "axis" to group
.attr("transform", "translate(0," + (h - padding) + ")") // shift the axis to bottom
.call(xAxis); // call the axis function
gs.exit().remove();
}
res = dataGenerator("Europe");
dataset = res[0];
dataset2 = res[1];
texts(dataset,dataset2);
d3.select("#selector").on("change", function() {
cont = d3.select(this).property('value');
res = dataGenerator(cont)
dataset = res[0]
dataset2 = res[1]
//svg.selectAll(".sym").remove()
texts(dataset,dataset2);
});
It basically gets the data and generates stacked bars. When user uses the select element on the page, it updates the data and generates new results. It successfully gets the first results and when user selects another option, it makes it happen also. But when user tries to use select part once again. It only generates bars for dataset's first item.
So, in this particular case I have countries and their data as numbers, at first load and first update it successfully shows all but when it comes to second update, it only generate bars for first country in dataset. It's been hours that I'm trying to fix this. I know I only have a little mistake but couldn't make it to solve.
Also here's the jsfiddle of the code: https://jsfiddle.net/510ep9ux/4/
Since I'm new at d3.js, I may not understand the update concept well.
So, any guesses?
Solved, using two separate functions textsInit and textsUpdate :
https://jsfiddle.net/qxqdp36x/2/
Essentially you need to separate initialization and update logic, and avoid re-creating elements when updating, that causes unintended behaviours.
Also, the variables gs and asd needs to be global to be accessible to both functions.
var textsInit = function(ds, ds2) {
var stack = d3.layout.stack();
stack_data = stack(ds);
var xScale = d3.scale.ordinal()
.domain(d3.range(ds[0].length))
.rangeRoundBands([0, w - offset], 0.50);
var yScale = d3.scale.linear()
.domain([0,
d3.max(stack_data, function(d) {
return d3.max(d, function(d) {
return d.y0 + d.y - 20;
});
})
])
.range([padding, h - 50]);
var xAxis = d3.svg.axis()
.scale(xScale)
.orient("bottom")
.ticks(ds[0].length);
gs = svg.selectAll("g").data(stack_data);
bars = gs.enter();
bars.append("g")
.attr("class", "stacked_bars")
.attr("fill", function(d, i) {
return colors(i);
});
asd = gs.selectAll("rect").data(function(d) {
return d;
});
asd.enter().append("rect");
asd.transition()
.duration(1000)
.attr("x", function(d, i) {
return xScale(i);
})
.attr("y", function(d) {
return h - yScale(d.y0) - yScale(d.y);
})
.attr("height", function(d) {
return yScale(d.y);
})
.attr("width", xScale.rangeBand())
.attr("class", "rectbar");
gs.append("g") // add a group element to the svg
.attr("class", "axis") //Assign class "axis" to group
.attr("transform", "translate(0," + (h - padding) + ")") // shift the axis to bottom
.call(xAxis); // call the axis function
}
And:
var textsUpdate = function(ds, ds2) {
var stack = d3.layout.stack();
stack_data = stack(ds);
var xScale = d3.scale.ordinal()
.domain(d3.range(ds[0].length))
.rangeRoundBands([0, w - offset], 0.50);
var yScale = d3.scale.linear()
.domain([0,
d3.max(stack_data, function(d) {
return d3.max(d, function(d) {
return d.y0 + d.y - 20;
});
})
])
.range([padding, h - 50]);
gs.data(stack_data);
asd = gs.selectAll("rect").data(function(d) { return d; });
asd.transition()
.duration(1000)
.attr("x", function(d, i) {
return xScale(i);
})
.attr("y", function(d) {
return h - yScale(d.y0) - yScale(d.y);
})
.attr("height", function(d) {
return yScale(d.y);
})
.attr("width", xScale.rangeBand())
.attr("class", "rectbar");
}
Edited to fix a small bug, updating the asd selection's data.
I made 2 simple but crucial changes to your code.
https://jsfiddle.net/guanzo/510ep9ux/6/
From
gs = svg.selectAll("g").data(stack_data);
to
gs = svg.selectAll("g.stacked_bars").data(stack_data);
The axis is also contained in a g element, so you have to ensure you're only selecting elements that are used for your data, and not unrelated elements.
From
gs.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + (h - padding) + ")")
.call(xAxis);
to
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + (h - padding) + ")")
.call(xAxis);
If you go into the browser inspector you'll see that you have an axis element for EVERY stacked_bars element, you only need 1 obviously. It only looks like there's 1 axis because they're absolutely positioned and stacked on top of each other.
I changed it so that the axis is appended when the svg is created, and every time new data is selected, the axis will update itself.

Creating multi-scale, nested, ordinal bar charts with D3

I am making an interactive version of this infographic
This is proving difficult in D3. There will be multiple ordinal scales:
One Y scale for each country
One Y scale for each period in each country
One X scale
For each period, there will be two values (visualized as two different colored bars). The main problem is that not all countries will have three periods; the data does not take a completely even shape. This is to say that some segments of the first Y scale will have different heights. I'm not sure how to take care of this.
Suppose I have this data:
CountryName Period ValueA ValueB
Argentina 2004-2008 12 5
Argentina 2004-2013 10 5
Argentina 2008-2013 8 4
Bolivia 2002-2008 4 2
Bolivia 2002-2013 6 18
Brazil 2003-2008 9 2
Brazil 2003-2013 2 19
Brazil 2008-2013 1 3
And I use the d3.nest() function:
d3.nest()
.key(function(d) { return d.CountryName; })
.key(function(d) { return d.Period; })
.map(data);
Now I'll have the data in the form that I want it, but note that there is some missing data - Bolivia only has two periods of data, in this case. I have created a JSFiddle for this, but as you can see, it has some problems. The height of the bars should be the same, always. I want to use yScale.rangeBand(), but the problem is that some countries will have three periods, where some will only have two. I also need to find a way to display the country names and periods to the left of the bars
If anybody has a better means of approaching this problem, please let me know. I have been struggling for this for a couple of days. As you can see from the JSFiddle, I only have one yScale but I'm sure it's preferable to use two given my situation - I do not know how to implement this.
Any and all help is greatly appreciated
You may not really need the nesting.
This is a special case bar chart so you will need to make axis' bars ticks grids all by yourself.
I have added comments in the code for you to follow
var outerWidth = 1000;
var outerHeight = 500;
var margin = {
left: 100,
top: 0,
right: 100,
bottom: 90
};
var barPadding = 0.6;
var barPaddingOuter = 0.3;
var innerWidth = outerWidth - margin.left - margin.right;
var innerHeight = outerHeight - margin.top - margin.bottom;
//make your svg
var svg = d3.select("body").append("svg")
.attr("width", outerWidth)
.attr("height", outerHeight);
var g = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + 30 + ")");
//the axis is on the right
var xAxisG = g.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + 10 + ")")
//the y axis is common for all
var yAxisG = g.append("g")
.attr("class", "y axis");
var xScale = d3.scale.linear().range([0, innerWidth]);
var yScale = d3.scale.ordinal().rangeRoundBands([0, innerHeight], barPadding, barPaddingOuter);
var yAxis = d3.svg.axis().scale(yScale).orient("left")
.outerTickSize(0);
function render(data) {
var xAxis = d3.svg.axis().scale(xScale).orient("top")
//.tickValues([-5, -4, -3, 0, 3, 5, 10, 15, 20, 25])
.outerTickSize(0)
.innerTickSize(-1 * outerHeight);
xScale.domain([0, 30]);
//this will make the y axis labels.
var country = "";
data.forEach(function(d) {
if (d.CountryName == country) {
d.label = d.Period
} else {
d.label = d.Period
d.heading = d.CountryName;
}
country = d.CountryName;
});
var labels = data.map(function(d) {
return d.label
});
//set the domain for all y labels,
yScale.domain(labels);
//makes the x Axis
xAxisG.call(xAxis);
//makes the y Axis
yAxisG.call(yAxis);
//make the bar chart for valueB
var bars = g.selectAll(".ValueA").data(data);
bars.enter().append("rect")
.attr("height", yScale.rangeBand() / 2);
bars
.attr("x", function(d) {
return xScale(0)
})
.attr("y", function(d) {
return yScale(d.label);
})
.attr("width", function(d) {
console.log(d.ValueA)
return xScale(d.ValueA);
})
.style("fill", "red")
.attr("class", "ValueA");
//make the bar chart for valueB
var bars = g.selectAll(".ValueB").data(data);
bars.enter().append("rect")
.attr("height", yScale.rangeBand() / 2);
bars
.attr("x", function(d) {
return xScale(0)
})
.attr("y", function(d) {
return yScale.rangeBand() / 2 + yScale(d.label);
})
.attr("width", function(d) {
return xScale(d.ValueB);
})
.style("fill", "blue")
.attr("class", "ValueA");
//make grid lines
var lines = g.selectAll(".xLine").data(data.filter(function(d){return !(d.heading == undefined) }));
lines.enter().append("line")
lines
.attr("x1", 0)
.attr("y1", function(d) {
return (yScale(d.label) - yScale.rangeBand())
})
.attr("x2", innerWidth)
.attr("y2", function(d) {
return (yScale(d.label) - yScale.rangeBand())
})
.style("stroke", "blue")
.attr("class", "xLine")
.style("display", function(s) {
if((yScale(s.label) - yScale.rangeBand()) < 0){
return "none";//not show grids when y comes negative
}
});
//make heading
var headings = g.selectAll(".heading").data(data.filter(function(d){return !(d.heading == undefined) }));
headings.enter().append("text")
.text(function(d){return d.heading})
.attr("x", -100)
.attr("y", function(d) {
return (yScale(d.label) - yScale.rangeBand()) +30
})
}
function type(d) {
d.ValueA = +d.ValueA;
d.ValueB = +d.ValueB;
console.log(d)
return d;
} d3.csv("https://gist.githubusercontent.com/cyrilcherian/e2dff83329af684cde78/raw/f85ce83497553c360d2af5d5dcf269390c44022f/cities.csv", type, render);
.tick line {
opacity: 0.2;
}
.xLine {
opacity: 0.2;
}
.axis text {
font-size: 10px;
}
.axis .label {}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.y.axis path,
.y.axis line {
stroke: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
Hope this helps!

Syntax for axis transition in svg/d3

I've an incredibly basic syntax question. I've been learning d3, SVG, and Javascript mostly by editing someone else's code, which is challenging.
The goal is to update a y axis after updating the data and the scale, which is based on the data. I want the axis--ticks and labels and all--to transition with the domain of the data. The axis isn't getting updated. The problem might be related to scope, or I'm referencing the wrong SVG element. (There are actually several plots getting updated simultaneously, but I'm just focusing on the axis of one of them here.)
function makeYaxis (chart, scale, nticks, label, width, height, xmf, visName)
{
var yAxis = d3.svg.axis()
.scale(scale)
.orient("left")
.ticks(nticks);
chart.append("svg:g")
.attr("class","y axis")
.append("g")
.attr("transform","translate(60,1)") // fix magic #
.call(yAxis);
var xMove = xmf.yylMarginFactor * width - 1;
var yMove = (((1 - xmf.xxbMarginFactor) * height +
xmf.xxtMarginFactor * height) / 2);
chart.append("svg:text")
.attr("class", visName + "xLabel")
.attr("x", 0)
.attr("y", 0)
.attr("dy", "-2.8em")
.text(label)
.attr("transform", "rotate(-90) translate(-" + yMove + "," + xMove + ")");
}
function makeYscale (plotMargin, thedata, xmf)
{
var dataMin = d3.min(thedata[0]);
var dataMax = d3.max(thedata[0]);
var yyMargin = d3.max([plotMargin * (dataMax - dataMin),0.05]);
var yy = d3.scale.linear()
.domain([dataMin - yyMargin, yyMargin + dataMax])
.range([(1 - xmf.xxbMarginFactor) * height, xmf.xxtMarginFactor * height]);
return yy;
}
// Set up this visualization
var chart = d3.select("#vis1")
.append("svg:svg")
.attr("class", "vis1chart")
.attr("width", width)
.attr("height", height);
var yy = makeYscale(plotMargin, thetas, xmf);
makeYaxis(chart, yy, nYTicks, "parameter", width, height, xmf, "vis1");
var points = chart.selectAll("circle.vis1points")
.data(thetas)
.enter().append("svg:circle")
.attr("class", "vis1points")
.attr("cx", function (d, i) { return xx(i); })
.attr("cy", function (d, i) { return yy(d); })
.attr("r", 3);
points.transition()
.attr("cx", function (d, i) { return xx(i); })
.attr("cy", yy)
.duration(dur);
vis1move();
function vis1move ()
{
function movePoints ()
{
var tmp = chart.selectAll(".vis1points").data(thetas[0], function (d, i) { return i; } );
var opa1 = points.style("stroke-opacity");
var opa2 = points.style("fill-opacity");
tmp
.enter().insert("svg:circle")
.attr("class", "vis1points")
.attr("cx", function (d, i) { return xx(i); })
.attr("cy", yy)
.attr("r", 3)
.style("stroke-opacity", opa1)
.style("fill-opacity", opa2);
tmp.transition()
.duration(10)
.attr("cx", function (d, i) { return xx(i); })
tmp.exit()
.transition().duration(10).attr("r",0).remove();
};
// (Code for updating theta, the data, goes here)
// Update axes for this vis
yy = makeYscale(plotMargin, thetas, xmf);
var transition = chart.transition().duration(10);
var yAxis = d3.svg.axis()
.scale(yy)
.orient("left")
.ticks(4);
transition.select("y axis").call(yAxis);
movePoints(); // Previously had been before axis update (fixed)
}
Sorry I can't get this self-contained.
Is transition.select.("y axis").call(yAxis) correct? Is there anything glaringly off here?
Edit: To keep things simple, I now have
// Update axes for this vis
yy = makeYscale(plotMargin, thetas, xmf);
var yAxis = d3.svg.axis().scale(yy);
chart.select("y axis").transition().duration(10).call(yAxis);
I'm wondering if something in the way I created the axis in the first place (in makeYaxis()) prevents me from selecting it properly. The yy function is returning the right values under the hood, and the data points are getting plotted and rescaled properly. It's just that the axis is "stuck."
Following meetamit's suggestions and the example here, I have stumbled on what appears to be a solution.
First, in function makeYaxis(), I removed the line .append("g"). I also updated the class name to yaxis. The function now reads
function makeYaxis (chart, scale, nticks, label, width, height, xmf, visName)
{
var yAxis = d3.svg.axis()
.scale(scale)
.orient("left")
.ticks(nticks);
chart.append("svg:g")
.attr("class","yaxis") // note new class name
// .append("g")
.attr("transform","translate(60,1)")
.call(yAxis);
// everything else as before
}
I then added an extra period in my call to select(".yaxis"):
chart.select(".yaxis").transition().duration(10).call(yAxis);
I would be very grateful if anyone could explain to me exactly why this solution appears to work.

Categories