I am using NVD3js with lineChart graph,
My dataset contains timestamps and values for 24 hour view, each point at xx:30,
example:
(00:30 - val_X, 01:30 - val_Y, 02:30 - val_Z, ...,23:30 - val_W)
[{"values":[{"x":1471552200000,"y":64262.58061338499},{"x":1471555800000,"y":62678.866737321965},{"x":1471559400000,"y":61750.00767023745},{"x":1471563000000,"y":61326.61449656117},{"x":1471566600000,"y":60561.389682317065},{"x":1471570200000,"y":60144.93124866629},{"x":1471573800000,"y":59785.83442165563},{"x":1471577400000,"y":69062.50859207465},{"x":1471581000000,"y":75910.99616145018},{"x":1471584600000,"y":75444.86937388242},{"x":1471588200000,"y":74308.98109650609},{"x":1471591800000,"y":73645.18914600794},{"x":1471595400000,"y":72899.18083184483},{"x":1471599000000,"y":72500.14363895029},{"x":1471602600000,"y":71881.80897423408},{"x":1471606200000,"y":71608.16164435333},{"x":1471609800000,"y":71560.75929676845},{"x":1471613400000,"y":71693.39707357567},{"x":1471617000000,"y":71662.43123936957},{"x":1471620600000,"y":70112.4723063366},{"x":1471624200000,"y":67639.43208386854},{"x":1471627800000,"y":65567.79922797237},{"x":1471631400000,"y":64098.448319984534},{"x":1471635000000,"y":62591.10055950839},{"x":1471638600000,"y":61341.82034098393},{"x":1471642200000,"y":60428.577018083095}],"key":"System Score","color":"#325c80","area":true}]
When I display it on a lineChart graph I see x domain from 00:30 to 23:30, I wanted to present 00:00 - 23:59, so I added 23:30 the previous day and 00:30 the next day and then I set the xDomain to [00:00, 23:59], However, the points are a bit off from its correct location althought the data is correct.
Is there anoter way to solve that problem?
Graph init:
nv.addGraph(function() {
score_graph = nv.models.lineChart()
.xScale(d3.time.scale())
.useInteractiveGuideline(true)
.showLegend(false)
;
score_graph.xAxis
.axisLabel('')
;
score_graph.yAxis
.axisLabel('y axis')
.ticks(5)
.tickFormat(d3.format('.3s'))
;
return score_graph;
});
Graph update data:
startTime is 00:00 and endTime is 23:59 of same day,
dataset has also 23:30 of previous day and 00:30 of next day points
var tooltip_obj = score_graph.interactiveLayer.tooltip;
tooltip_obj.headerFormatter(function(d, i) {
return graphLocales.timeFormat(time_format)(new Date(+d));
});
score_graph.xAxis
.tickFormat( function(d) { return graphLocales.timeFormat(time_format)(new Date(+d)); } );
var line_chart_data = d3.select('#score-history-block svg').datum(parsed_data_system);
var y_max = d3.max(parsed_data_system[0].values, function(d) { return +d.y; }) * 1.2;
if (y_max < 100) y_max = 100;
score_graph.yDomain([0, y_max] );
score_graph.xDomain([startTime.toDate(), endTime.toDate()]);
line_chart_data.transition().duration(500)
.call(score_graph)
;
Related
Hello i have a question about dc/d3, i would like to show on the xAxis the monthname, Januar till December (in correct order)
The Value should be sum up for each year (i mean on January shoud be the Values from Year 2020 till 2022 - only one Januar)
the following code works fine for Bars because the reorder of the x in this way works fine, but for line chart i get a funny chart.
function Dimension() {
return Data.ndx.dimension(function (d) { return moment.localeData()._months[d["DATE"].getMonth()]; });
}
function Group(thisDim) {
var thisVal = thisDim.group().reduceSum(function (d) { return d["VALUE"]; });
return thisVal;
}
var thisDim = Dimension();
var thisVal = Group();
chartObject.dimension(thisDim).group(thisVal)
.title(function (d) { return d.key; })
.ordinalColors(['#3182bd', '#6baed6', '#9ecae1', '#c6dbef', '#dadaeb'])
.label(function (d) { return d.key;})
.margins({top: 10, right: 10, bottom: 30, left: 70})
.elasticY(true)
;
for (var i=0;i<12;i++) x.push(moment.localeData()._months[i]);
chartObject.x(d3.scaleBand().domain(x)).xUnits(dc.units.ordinal);
This is a simple code (i hope i have put all in)
Here is a screenshot of the chart:
I want to add spacing between 0 and March in the x axis so that it looks better.
startdate and enddate are calculated by d3.min and d3.max.
So those columns looks bad because of their width.
var x = d3.scaleTime().range([0, width]);
var datestart = d3.min(data, function(d) { return parseDate(d.date); });
var dateend = d3.max(data, function(d) { return parseDate(d.date);});
x.domain([datestart, dateend]);
How can I do this? How to add a padding using d3.scaleTime()?
As you mentioned in your comment (maybe a reply?), there is no padding in a time scale.
Therefore, the problem here is one that from time to time (no pun intended) appears at SO: you are using the wrong tool for the task. Bar charts should always use a categorical (qualitative) scale, not a quantitative scale or a time scale.
However, if you really need to use the time scale here (for whatever reason), you can add the padding in the domain, using offset.
For instance, this will subtract 15 days at the beginning of your domain, and add 15 days at the end:
var datestart = d3.min(data, function(d) {
return d3.timeDay.offset(parseDate(d.date), -15);
//subtract 15 days here ---------------------^
});
var dateend = d3.max(data, function(d) {
return d3.timeDay.offset(parseDate(d.date), 15);
//add 15 days here -------------------------^
});
You can tweak that value until you have an adequate padding. As I don't know how you are calculating the width of the bars (again, bars should not be used in a time scale), 15 days is just a guess here.
I'm constructing a graph right now which is taking in data from a postgres backend. For the construction of the x-axis, I have the following:
var x = d3.scaleTime()
.domain(d3.extent(data_prices, function(d){
var time = timeParser(d.timestamp);
return time;
}))
.range([0,width])
where timeParser is a function representing d3.timeParse().
I have a data point which is at 16:58 and another at 22:06 and it looks a little ugly having it just stick at the side like that. How would I say, for instance, have there be a slight padding of say, +/- 30 minutes for each and continue the trendline path on each end? (or at least just the first part)
To create a padding in a time scale, use interval.offset. According to the API:
Returns a new date equal to date plus step intervals. If step is not specified it defaults to 1. If step is negative, then the returned date will be before the specified date.
Let's see it working. This is an axis based on a time scale with a min and a max similar to yours:
var svg = d3.select("svg");
var data = ["16:58", "18:00", "20:00", "22:00", "22:06"].map(function(d) {
return d3.timeParse("%H:%M")(d)
});
var scale = d3.scaleTime()
.domain(d3.extent(data))
.range([20, 580]);
var axis = d3.axisBottom(scale);
var gX = svg.append("g")
.attr("transform", "translate(0,50)")
.call(axis)
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="600" height="100"></svg>
Now, to create the padding, we just need to subtract and add 30 minutes at the extremes:
var scale = d3.scaleTime()
.domain([d3.timeMinute.offset(d3.min(data), -30),
//subtract 30 minutes here --------------^
d3.timeMinute.offset(d3.max(data), 30)
//add 30 minutes here ------------------^
])
.range([20, 580]);
Here is the result:
var svg = d3.select("svg");
var data = ["16:58", "18:00", "20:00", "22:00", "22:06"].map(function(d) {
return d3.timeParse("%H:%M")(d)
});
var scale = d3.scaleTime()
.domain([d3.timeMinute.offset(d3.min(data), -30), d3.timeMinute.offset(d3.max(data), 30)])
.range([20, 580]);
var axis = d3.axisBottom(scale);
var gX = svg.append("g")
.attr("transform", "translate(0,50)")
.call(axis)
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="600" height="100"></svg>
Have in mind that this solution does not round the extreme ticks to the nearest half hour: it adds and subtracts exactly half an hour.
So, to round to the nearest half hour, you can do a simple math using Math.floor and Math.ceil:
.domain([
d3.min(data).setMinutes(Math.floor(d3.min(data).getMinutes() / 30) * 30),
d3.max(data).setMinutes(Math.ceil(d3.max(data).getMinutes() / 30) * 30)
])
Here is the demo:
var svg = d3.select("svg");
var data = ["16:58", "18:00", "20:00", "22:00", "22:06"].map(function(d) {
return d3.timeParse("%H:%M")(d)
});
var scale = d3.scaleTime()
.domain([d3.min(data).setMinutes(Math.floor(d3.min(data).getMinutes() / 30) * 30), d3.max(data).setMinutes(Math.ceil(d3.max(data).getMinutes() / 30) * 30)])
.range([20, 580]);
var axis = d3.axisBottom(scale);
var gX = svg.append("g")
.attr("transform", "translate(0,50)")
.call(axis)
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="600" height="100"></svg>
Background
I'm using D3 4.x to build a multi-series line chart (see an example) to compare multiple years of annual data, where the Y-axis is the range of possible values throughout the years and the X-axis represents the time interval between the dates of 01/01-12/31.
Issues
While building this chart, it appears as though time intervals are year specific preventing data from different years to be rendered on the same chart. To reiterate, the current approach is to overlaying each year on the same chart; a line for each year. And to do this with time intervals being year specific, requires significant (and erroneous) data manipulation.
In the example chart below, one can see that the year 2014 approaches from the left (color: blue) and 2015 continues on to the right (color: orange). What I'd like to see is both of these to overlay on top of each other.
Acknowledgement / Caveat
Some of the complexities of comparing multiple years worth of data by overlaying the data on top of each other (ie. multiple lines, each representing a year) are acknowledged. Such as how to deal with years with leap days compared with those that do not. This complicates thing programmatically but visually should not be an issue.
Questions
1) What is the best approach to using time intervals when the year just gets in the way?
2) Is there a better, more elegant, approach to creating multi-series line charts that represent multiple years layered on top of each other, instead of hacking together the pieces?
3) Is there a way (not statically) to remove the year (ie. 2015) from the axis and use the month (January) instead?
4) Is it possible to shift the text on the X-axis to the right (programmatically), so that the month appears between the tick marks (IMO, it's visually more accurate)?
There are several ways to solve this. Actually, so many of them that this question qualifies as too broad. Every approach, of course, has its pros and cons. For instance, a simple one is dropping the time scale altogether, and using a point scale for the x axis.
I'll suggest a different approach, a little bit more complex, but still using time scales: create several different time scales, one for each year. Each of them has the same range: that way, the lines for each year can overlay on top of each other.
For instance, from 2013 to 2017:
var years = [2013, 2014, 2015, 2016, 2017];
var scales = {};
years.forEach(d => {
scales["scale" + d] = d3.scaleTime()
.domain([new Date(+d, 0, 1), new Date(+d, 11, 31)])
.range([30, 470]);
});
That way, you can keep using a time scale for the x axis. The problem is this: time scales deal with each year as they are: different. Some years have more days, some of them have less days. Even the days can have more hours or less hours.
So, I chose one year (2017) and displayed only the months for that year in the x axis. The result may not be extremely precise, but it's good enough if you have an entire year in the x axis.
Here is the demo. The data is randomly generated, each line is a different year (from 2013 to 2017), going from Jan 1st to Dec 31st:
var svg = d3.select("body").append("svg")
.attr("width", 500)
.attr("height", 200);
var color = d3.scaleOrdinal(d3.schemeCategory10);
var thisYear;
var line = d3.line()
.x(d => scales[thisYear](d.x))
.y(d => yScale(d.y))
.curve(d3.curveMonotoneX);
var years = [2013, 2014, 2015, 2016, 2017];
var data = [];
years.forEach((d, i) => {
data.push({
year: d,
points: []
});
for (var j = 0; j < 12; j++) {
data[i].points.push({
y: Math.random() * 80 + 20,
x: new Date(d, j)
})
}
});
var scales = {};
var yScale = d3.scaleLinear()
.domain([0, 100])
.range([170, 30]);
years.forEach(d => {
scales["scale" + d] = d3.scaleTime()
.domain([new Date(+d, 0, 1), new Date(+d, 11, 31)])
.range([30, 470]);
});
var paths = svg.selectAll("foo")
.data(data)
.enter()
.append("path");
paths.attr("stroke", (d, i) => color(i))
.attr("d", d =>{
thisYear = "scale" + d.year;
return line(d.points);
})
.attr("fill", "none");
var xAxis = d3.axisBottom(scales.scale2017).tickFormat(d=>d3.timeFormat("%b")(d));
var yAxis = d3.axisLeft(yScale);
var gX = svg.append("g").attr("transform", "translate(0,170)").call(xAxis);
var gY = svg.append("g").attr("transform", "translate(30,0)").call(yAxis);
<script src="https://d3js.org/d3.v4.js"></script>
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"));