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.
Related
How do i specify the axis range in React-Vis
For Ex:
0 to 100 for Y Axis and the data is as below
data={[ {x: 1, y: 45}, {x: 2, y: 50}, {x: 3, y: 85} ]}/>
The YAxis must range 0 to 100 even though the max Y value here is 85
Use the xDomain or yDomain props on the XYPlot component <XYPlot xDomain={[0, 50]}
From their docs about scales at:
https://uber.github.io/react-vis/documentation/general-principles/scales-and-data
"To redefine a scale, you must pass a prop to the series that uses that scale. The prop names are based on the name of the attribute: name + Domain, name + Range, name + Type, name + Padding (for instance: yDomain, colorType, xRange)."
So for your purpose, you would set <XYPlot yDomain=[0,100]></XYPlot>
Hi I'm trying to set an ordinal scale for a line graph in d3 using v4. For some reason the ticks do not scale properly although I have scaled them as such:
var yTicks = d3.scaleOrdinal()
.domain(["apple", "orange", "banana", "grapefruit", "mango"])
.range([0, h])
var xAxis = d3.axisBottom().scale(x).tickSize(-h);
// var xAxis = d3.svg.axis().scale(x).tickSize(-h).tickSubdivide(true);
var yAxisLeft = d3.axisLeft().scale(yTicks);
// Add the x-axis.
graph.append("svg:g").attr("class", "x axis").attr("transform", "translate(0," + h + ")").call(xAxis);
// add lines
// do this AFTER the axes above so that the line is above the tick-lines
for (var i = data.length - 1; i >= 0; i--) {
graph.append("svg:path").attr("d", line(data[i])).attr("class", "data" + (i + 1));
};
graph.append("svg:g").attr("class", "y axis").attr("transform", "translate(0,0)").call(yAxisLeft);
The full version of what I've done can be found at this fiddle here: https://jsfiddle.net/5g1fe6qd/
You cannot set the range of an ordinal scale the way you did here: you have to specify the discrete values.
An easy solution in using a point scale instead:
var yTicks = d3.scalePoint()
Here is your updated fiddle: https://jsfiddle.net/5g1fe6qd/1/
This is expected behavior:
ordinal.range([range])
If range is specified, sets the range of the ordinal scale to the
specified array of values. The first element in the domain will be
mapped to the first element in range, the second domain value to the
second range value, and so on. If there are fewer elements in the
range than in the domain, the scale will reuse values from the start
of the range. If range is not specified, this method returns the
current range.
(emphasis mine, from API documentation)
You've only specified two elements in your range, therefore, the five values in your domain are mapped to these two values in the range (hence the overlapping text). You could use something along these lines:
d3.scaleOrdinal()
.domain(["apple", "orange", "banana", "grapefruit", "mango"])
.range([0, h*0.25, h*0.5, h*0.75, h]
(fiddle: https://jsfiddle.net/bmysrmcd/)
However, Gerardo's answer provides an alternative that doesn't require you set map each element in the domain to the range, and that is a better solution.
In my d3 line chart, I only want ticks for the plotted data. This proves to be a issue with time stamps though as I get:
d3.js:7651 Error: <g> attribute transform: Expected number, "translate(NaN,0)"..
I thought to convert the strings to numbers in the tickValues array but I can not since it's got a colon. Any ideas?
// Hard coded data
scope.data = [
{date: '12:00', glucoseLevel: 400},
{date: '15:00', glucoseLevel: 200},
{date: '18:00', glucoseLevel: 300},
{date: '23:00', glucoseLevel: 400}
];
var parseDate = d3.timeParse('%I:%M');
scope.data.forEach(function(d) {
d.date = parseDate(d.date);
d.glucoseLevel = +d.glucoseLevel;
});
var x = d3.scaleTime()
.range([0, width]);
var xAxis = d3.axisBottom(x)
.tickValues(['12:00', '15:00', '18:00', '23:00']);
// Add the X Axis
svg.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0,' + height + ')')
.call(xAxis);
You are specifying X values as times, so you must also specify the X-axis tick values as times.
As you already have the X values in the correct format, you can just write
var xAxis = d3.axisBottom(x)
.tickValues(scope.data.map(function (d) { return d.date; }));
.tickValues() isn't for setting the tick labels, it's for setting where on the axis the ticks appear. If you want the tick labels formatted in some way, specify a formatter using tickFormat, for example:
var xAxis = d3.axisBottom(x)
.tickValues(scope.data.map(function (d) { return d.date; }))
.tickFormat(d3.timeFormat("%H:%M"));
I've used the format string %H:%M instead of %I:%M as %I is hours in the range 01-12 whereas %H uses the 24-hour clock. For consistency I'd recommend changing your time parsing function to d3.timeParse('%H:%M'), although parsing a time with the hours greater than 12 using %I seems to work.
Finally, you'll also need to set the domain of your scale object x, for example:
var x = d3.scaleTime()
.domain([parseDate('12:00'), parseDate('23:00')])
.range([0, width]);
The two values passed to domain are the minimum and maximum X values to use for the axis. I've used the minimum and maximum values of your data, but I could have chosen a different time range (e.g. 00:00 to 24:00) as long as it contained all of your data points.
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.
I have this simple linear scale:
var x = d3.scale.linear().domain([0, 250]);
x.ticks(6), as expected, returns:
[0, 50, 100, 150, 200, 250]
However, x.ticks(11) returns:
[0, 20, 40, 60, 80, 100, 120, 140, 160, 180, 200, 220, 240]
When what I want is:
[0, 25, 50, 75, 100, 125, 150, 175, 200, 225, 250]
How do I fix this?
I had a similar issue with ordinal scales, I simply wrote some code to pick evenly spaced intervals in my data. Since I wanted it to always choose the first and last data element on the axis, I calculate the middle part only. Since some things do not divide evenly, rather than having the residual in one or two bins, I spread it out across the bins as I go; until there is no more residual.
There is probably a simpler way to accomplish this but here's what I did:
function getTickValues(data, numValues, accessor)
{
var interval, residual, tickIndices, last, i;
if (numValues <= 0)
{
tickIndices = [];
}
else if (numValues == 1)
{
tickIndices = [ Math.floor(numValues/2) ];
}
else
{
// We have at least 2 ticks to display.
// Calculate the rough interval between ticks.
interval = Math.floor(data.length / (numValues-1));
// If it's not perfect, record it in the residual.
residual = Math.floor(data.length % (numValues-1));
// Always label our first datapoint.
tickIndices = [0];
// Set stop point on the interior ticks.
last = data.length-interval;
// Figure out the interior ticks, gently drift to accommodate
// the residual.
for (i=interval; i<last; i+=interval)
{
if (residual > 0)
{
i += 1;
residual -= 1;
}
tickIndices.push(i);
}
// Always graph the last tick.
tickIndices.push(data.length-1);
}
if (accessor)
{
return tickIndices.map(function(d) { return accessor(d); });
}
return tickIndices.map(function(i) { return data[i]; });
}
You call the function via:
getTickvalues(yourData, numValues, [optionalAccessor]);
Where yourData is your array of data, numvalues is the number of ticks you want. If your array contains a complex datastructure then the optional accessor comes in handy.
Lastly, you then feed this into your axis. Instead of ticks(numTicks) which is only a hint to d3 apparently, you call tickValues() instead.
I learned the hard way that your tickValues have to match your data exactly for ordinal scales. This may or may not be as helpful for linear scales, but I thought I'd share it anyways.
Hope this helps.
Pat
You can fix this by replacing the x.ticks(11) with your desired array.
So if your code looks like this and x is your linear scale:
chart.selectAll("line")
.data(x.ticks(11))
.enter()
.append("line")
.attr("x1", x)
.attr("x2", x)
.attr("y1", 0)
.attr("y2",120)
.style("stroke", "#CCC");
You can replace x.ticks(11) with your array:
var desiredArray = [0, 25, 50, 75, 100, 125, 150, 175, 200, 225, 250]
chart.selectAll("line")
.data(desiredArray)
.enter()
.append("line")
.attr("x1", x)
.attr("x2", x)
.attr("y1", 0)
.attr("y2",120)
.style("stroke", "#CCC");
The linear scale will automatically place your desired axes based on your input. The reason why the ticks() isn't giving you your desired separation is because d3 just treats ticks() as a suggestion.
axis.tickvalues((function(last, values) {
var myArray = [0];
for(var i = 1; i < values; i++) {
myArray.push(last*i/(values-1))
}
return myArray;
})(250, 11));
This should give you an evenly spaced out array for specifying the number of tick values you want in a particular range.