D3.JS change text in axis ticks to custom strings - d3.js

I want to select each text element in each g.tick and change the text to be "2012", "2013", "2014", "2015" instead of 0, 4, 8, 12 respectively
I've tried first to grab all of these text elements as an array, but this was not working: var arrOfText = d3.selectAll("g.yaxis g.tick text")

Look into d3's axis.tickFormat([format]) function - it lets you specify how you want your data formatted.
Here's a simple example you can edit to fit your use case.
var xAxis = d3.svg.axis()
.scale(x)
.tickValues([1, 2, 3, 5, 8, 13, 21])
.tickFormat(function(d, i){ return "Num = " + d; });

Related

How to match up scaleBand with scaleLinear in D3.js

I have two series of scale, one is linear and the other is band, how can I make them to match up if there is some caps in the data.
Take a look at the example if necessary.
Mouse over and you see the boxes are not matching with the breaks of line.
If you want your scaleBand to be scaled (widened) where data is missing, I don't think that the scaleBand is the proper method for this, but it is unclear if that is something you want. Band scales are intended to provide equal spacing for each data value and that all values are present - it is an ordinal scale.
Assuming you only want the band scale to be aligned with your data where it is present:
If you log the domains of each of your x scales (scaleBand and scaleLinear) we find that the scaleBand has a domain of:
[ "1", "2", "8", "9", "13", "14", "20", "22" ] // 8 elements
And the scaleLinear has a domain of:
[ 1, 22 ] // a span of 22 'elements'
The scaleBand will need an equivalent domain to the scaleLinear. You can do this statically ( which I show mostly to demonstrate how d3.range will work):
let xBand = d3.scaleBand()
.domain(d3.range(1,23))
.rangeRound([0, width]);
This actually produces a domain that has 22 elements from 1 through 22.
or dynamically:
let xBand = d3.scaleBand()
.domain(d3.range(d3.min(testData1, d => d[0],
d3.max(testData1, d => d[0]+1)))
You could do this other ways, but the d3.range() function is nice and easy.
However, there is still one issue that remains, this is aligning the ticks between the two scales. For the linear scale, the tick for the first value (1) is on the y axis, but the band gap scale starts (and is not centered) on the y axis and fills the gap between 1 and 2. In other words, the center point of the band does not align vertically with the vertices of the line graph.
This can be addressed by adding 0.5 to both the lower and upper bounds of the linear scale's domain:
let xDomain = [
d3.min(testData1, d => d[0]-0.5),
d3.max(testData1, d => d[0]+0.5)
];
I've udpated your codepen with the relevant changes: codepen.
And in the event that that disappears, here is a snippet (the mouse over does not work for me for some reason in the snippet, it does in the codepen )
let width = 1000;
let height = 300;
let svg = d3.select(".wrapper-area-simple").append("svg")
.attr("width", width + 80)
.attr("height", height + 80)
.append('svg:g')
.attr('transform', 'translate(40, 30)');
let testData1 = [
[ 1, 10],
[ 2, 30],
[ 8, 34],
[ 9, 26],
[13, 37],
[14, 12],
[20, 23],
[22, 16],
];
let xDomain = [
d3.min(testData1, d => d[0]-0.5),
d3.max(testData1, d => d[0]+0.5)
];
let x = d3.scaleLinear()
.rangeRound([0, width])
.domain(xDomain);
let y = d3.scaleLinear()
.range([height, 0])
.domain(d3.extent(testData1, d => d[1]));
let line = d3.line()
.x(d => x(d[0]))
.y(d => y(d[1]));
svg.append('svg:g')
.datum(testData1)
.append('svg:path')
.attr('d', line)
.attr('fill', 'none')
.attr('stroke', '#000');
let xAxis = d3.axisBottom(x)
.ticks(testData1.length);
svg.append('svg:g')
.call(xAxis)
.attr('transform', `translate(0, 300)`);
let xBand = d3.scaleBand()
.domain(d3.range(d3.min(testData1, d => d[0]),
d3.max(testData1, d => d[0]+1)
))
.rangeRound([0, width]);
svg.append('svg:g')
.selectAll('rect')
.data(testData1)
.enter()
.append('svg:rect')
.attr('x', d => xBand(d[0]))
.attr('width', xBand.bandwidth())
.attr('height', height)
.attr('fill', '#000')
.on('mouseover', function() {
d3.select(this).classed('over', true);
})
.on('mouseout', function() {
d3.select(this).classed('over', false);
});
svg {
border: 1px solid red;
}
rect {
opacity: .1;
}
rect.over {
opacity: .2;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.5.0/d3.min.js"> </script>
<div class="wrapper-area-simple"></div>
Well, bad news for you: they will never match up (in your case). Let's see why.
This is your data:
let testData1 = [
[1, 10],
[2, 30],
[8, 34],
[9, 26],
[13, 37],
[14, 12],
[20, 23],
[22, 16],
];
As you can see, regarding the x coordinate, the line jumps from 1 to 2, but then from 2 to 8, from 8 to 9, and then from 9 to 13... That is, the x range intervals are not regular, evenly spaced. So far, so good.
However, when you pass the same data to the band scale, this is what it does: it divides the range ([0, width], which is basically the width) by testData1.length, that is, it divides the range by 8, and creates 8 equal intervals. Those are your bands, and that's the expected behaviour of the band scale. From the documentation:
Discrete output values are automatically computed by the scale by dividing the continuous range into uniform bands. (emphasis mine)
One solution here is simply using another linear scale:
let xBand = d3.scaleLinear()
.domain(xDomain)
.rangeRound([0, width]);
And this math to the width of the rectangles:
.attr('width', (d,i) => testData1[i+1] ? xBand(testData1[i+1][0]) - xBand(d[0]) : 0)
Here is your updated Codepen: http://codepen.io/anon/pen/MJdGyY?editors=0010

How to pass key/values to multi-line C3.JS Chart with automated timeseries x-axis?

I have timeseries data. On the server, I generate an x array, and three arrays of values (all of equal length).
However, the x array doesn't always go month to month. Sometimes it skips a couple of months. When this happens, the x-axis is very spaced out. Is there a good way to generate range of labels on the x-axis, and pass key values for different lines so that the entire line's values are still represented in the chart.
Currently I have:
var chart = c3.generate({
bindto: "#" + this.chart.chartId,
data: {
x: 'x',
columns: [
["x", "2015-01-01", "2015-02-01, "2015-06-01", "2016-01-01"],
["data1", 5, 8, 2, 9]
["data2", 3, 10, 2, 1]
["data3", 1, 8, 4, 9]
},
subchart: {
show: true
},
axis: {
x: {
type: 'timeseries',
extent: ['2015-01-01', '2016-01-01'],
tick: {
format: '%Y-%m-%d'
}
}
}
});
Any advice is appreciated to solve this spaced out issue for timeseries.
You appear to be asking about addressing x axes with c3, but your tags suggest that you are open to a solution with D3. Looking at your JSON string and seeing the node x.extent I would suggest trying something like this:
// setup x axes
var minDate = yourJSON.x.extent[0], maxDate = yourJSON.x.extent[1],
xScale = d3.time.scale().domain([maxDate,maxDate]).range([0, width]),
xAxis = d3.svg.axis().scale(xScale).orient("bottom").tickFormat(d3.time.format("%Y-%m-%d"));
var svg = d3.select("#chart-content").append("svg")...etc;
// x-axis
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
.append("text")
.attr("class", "label")
.attr("x", width)
.attr("y", -6)
.style("text-anchor", "end");
Since you are specifying a range, D3 will expose time in between those dates.

d3.js. Stream chart shifts with different data sets. Why?

Code available here http://jsfiddle.net/zeleniy/ea1uL7w9/
data = [47, 13, 61, 46, 26, 32, 6, 85, 1, 14, 86, 77, 13, 66, 0, 20, 11, 87, 5, 15];
data = [52, 33, 53, 45, 59, 45, 42, 50, 53, 50, 37, 45, 52, 50, 46, 48, 52, 56, 58, 59];
width = 300;
height = 100;
xScale = d3.scale.linear()
.domain([0, data.length])
.range([0, width]);
yScale = d3.scale.linear()
.domain([d3.min(data), d3.max(data)])
.range([0, height])
area = d3.svg.area()
.interpolate("basis")
.x(function(d, i) { return xScale(i); })
.y0(function(d) { return yScale(-d / 2); })
.y1(function(d) { return yScale(d / 2); });
svg = d3.select("#stream")
.append("svg")
.attr("width", width)
.attr("height", height);
svg.selectAll("path")
.data([this.data])
.enter()
.append("path")
.attr("transform", "translate(0," + (height / 2) + ")")
.style("fill", "red")
.attr("d", area);
With first data set chart drawn in the center of svg element, as i expect. But with second data set stream shifts to the top of svg element. And i can't understand why. So why?
The first array contains values close to 0 and it's opening up your range. This line, then, is a fudge to shift the path into that open window:
.attr("transform", "translate(0," + (height / 2) + ")")
That said, you are setting up your scales in a confusing way (to me at least). I think about my domain as the min/max of my (plotted) dataset, in your case -max(d/2) and max(d/2). Further, I also think about my y-scale going from bottom to top as it would in a normal plot. With these changes, you don't need to artificially move anything:
var dataMax = d3.max(data);
yScale = d3.scale.linear()
.domain([ -dataMax/2, dataMax/2 ]) // real min/max of plotted data
.range([height, 0]) //<- bottom to top, although it still works without this change...
In this example, I left an axis overlayed for illustration.

Can't get nvd3 x value labels to display right

I'm trying to specify the x axis labels as strings. I can get the strings to show up, but I can't get them to spread out/align properly. All of the numbers are displaying correctly, I just can't seem to get the labels to spread out correctly or the domain to show up. I'm really looking to get the labels working, but the domain seemed like it could be an alternate way possibly.
nv.addGraph(function() {
var chart = nv.models.lineChart();
var fitScreen = false;
var width = 600;
var height = 300;
var zoom = 1;
chart.useInteractiveGuideline(true);
chart.xAxis
.domain(["Sunday","Monday","Tuesday","Wednesday","Thursday"])
.tickValues(['Label 1','Label 2','Label 3','Label 4','Label 5'])
.ticks(5);
//.tickFormat(d3.format('d'));
chart.yAxis
.tickFormat(d3.format(',.2f'));
d3.select('#chart1 svg')
.attr('perserveAspectRatio', 'xMinYMid')
.attr('width', width)
.attr('height', height)
.datum(veggies);
setChartViewBox();
resizeChart();
return chart;
});
Data
veggies = [
{
key: "Peas",
values: [
{x: 0, y: 2},
{x: 1, y: 5},
{x: 2, y: 4}
]
},
{
key: "Carrots",
values: [
{x: 0, y: 2},
{x: 1, y: 5},
{x: 2, y: 4}
]
}
];
The "domain" of a d3 scale or axis is the input data. Which in your data set look like numbers, not names of the week. See http://alignedleft.com/tutorials/d3/scales
You want to something like xAxis.domain([0,1,2,3,4,5]);.
So how do you get labels to go with your numbers? Without thinking about it to closely, I suggested using the "range" (output) part of the scale. But range for the axis defines the numerical spacing for the categories on your graph, not the labels.
What you want is a custom formatting function that converts the numbers from the data into labels. You set that with the "tickFormat" option:
xAxis.tickFormat( function(index) {
var labels = ["Label0", "Label1", "Label2", "Label3", "Label4", "Label5"];
return labels[index];
});
Normally, tick formatting functions are used to format numbers into decimals or percents, but the syntax allows you to use any function that takes the data value as a parameter (I've named it index here) and returns the string that you want to be displayed. The line return labels[index]; finds the index-numbered element in the labels array, where the first label is index 0.
If your data values aren't consecutive integers starting with zero, you can use the object/associative array format, with named elements, instead of just an array. For example, if your data had the values "M", "T", "W", "R", etc., and you wanted it to display full day names, you would use:
xAxis.tickFormat(function(name) {
var labels = {"M":"Monday", "T":"Tuesday", "W":"Wednesday",
"R": "Thursday", "F":"Friday"};
return labels[name];
});
The values you pass to tickValues() have to be values in the input domain,* i.e, in the same format as the values in the JSON data. Also, once you set tickValues explicitly, don't over-ride that setting by setting a number of ticks. But you should only need tickValues if you only want some of the days to have labels (for example, if there is not enough space on the chart for all the names).
*Note correction.

Drawing Multiple Lines in D3.js

Up until now, I've been using loops to add line elements to a D3 visualization, but this doesn't seem in the spirit of the API.
Let's say I have got some data,
var data = {time: 1, value: 2, value2: 5, value3: 3,value4: 2},
{time: 2, value: 4, value2: 9, value3: 2,value4: 4},
{time: 3, value: 8, value2:12, value3: 2,value4:15}]);
I'd like four lines, with time as the X for all 4.
I can do something like this:
var l = d3.svg.line()
.x(function(d){return xScale(d[keys[0]]);})
.y(function(d,i){
return yScale(d[keys[1]]);})
.interpolate("basis");
var l2 = d3.svg.line()
.x(function(d){return xScale(d[keys[0]]);})
.y(function(d,i){
return yScale(d[keys[2]]);})
.interpolate("basis");
var l3 = d3.svg.line()
.x(function(d){return xScale(d[keys[0]]);})
.y(function(d,i){
return yScale(d[keys[3]]);})
.interpolate("basis");
var l4 = d3.svg.line()
.x(function(d){return xScale(d[keys[0]]);})
.y(function(d,i){
return yScale(d[keys[4]]);})
.interpolate("basis");
And then add these one by one (or by a loop).
var line1 = group.selectAll("path.path1")
.attr("d",l(data));
var line2 = group.selectAll("path.path2")
.attr("d",l2(data));
var line3 = group.selectAll("path.path3")
.attr("d",l3(data));
var line4 = group.selectAll("path.path4")
.attr("d",l4(data));
Is there a better more general way of adding these paths?
Yes. First I would restructure your data for easier iteration, like this:
var series = [
[{time: 1, value: 2}, {time: 2, value: 4}, {time: 3, value: 8}],
[{time: 1, value: 5}, {time: 2, value: 9}, {time: 3, value: 12}],
[{time: 1, value: 3}, {time: 2, value: 2}, {time: 3, value: 2}],
[{time: 1, value: 2}, {time: 2, value: 4}, {time: 3, value: 15}]
];
Now you need just a single generic line:
var line = d3.svg.line()
.interpolate("basis")
.x(function(d) { return x(d.time); })
.y(function(d) { return y(d.value); });
And, you can then add all of the path elements in one go:
group.selectAll(".line")
.data(series)
.enter().append("path")
.attr("class", "line")
.attr("d", line);
If you want to make the data structure format smaller, you could also extract the times into a separate array, and then use a 2D array for the values. That would look like this:
var times = [1, 2, 3];
var values = [
[2, 4, 8],
[5, 9, 12],
[3, 2, 2],
[2, 4, 15]
];
Since the matrix doesn't include the time value, you need to look it up from the x-accessor of the line generator. On the other hand, the y-accessor is simplified since you can pass the matrix value directly to the y-scale:
var line = d3.svg.line()
.interpolate("basis")
.x(function(d, i) { return x(times[i]); })
.y(y);
Creating the elements stays the same:
group.selectAll(".line")
.data(values)
.enter().append("path")
.attr("class", "line")
.attr("d", line);

Resources