D3 Chart - Using tickValues for Time Gives "translate(NaN,0)" - d3.js

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.

Related

Setting labels on axis ticks in D3 V5

I'm trying to set the x-axis labels to date strings I create. I tried using scaleTime() but found that to be a nightmare and not really required for what I want to do.
I've tried scaleLinear() and scaleOrdinal() with 'almost' success, but didn't get there totally
In the examples below 'xlabels' is an array of about 23 strings, like:
["02 JAN 2020", "03 JAN 2020" ... etc]
First trying scaleLinear:
function draw_Xscale() {
var xScale1 = d3.scaleLinear()
.domain([0, xlabels.length])
.range([0, width]);
const g = svg.append("g")
.attr("class", "axis")
.attr("transform", `translate(0, ${height - 10})`);
let x_axis = d3.axisBottom(xScale1).ticks(tlabels.length);
g.call(x_axis).selectAll("line,path").style("stroke", "brown");
}
This draws an axis but the tick labels are numbers [0 .. 22] - not what I want. I tried stuff from some online examples something like the following tacked on:
.selectAll("text")
.style("text-anchor", "end")
.attr("dx", "-.8em")
.attr("dy", ".15em")
.attr("transform", "rotate(-65)");
but simply couldnt get it to work - probably some subtlety of the syntax I don't understand.
I also tried scaleOrdinal:
function draw_OrdScale() {
let arr = Array.from({length: xlabels.length}, (e, i) => i);
var ordinalScale = d3.scaleOrdinal()
.domain(xlabels)
.range([0, width]);
const g = svg.append("g")
.attr("class", "axis")
.attr("transform", `translate(0, ${height-100})`);
let x_axis = d3.axisBottom(ordinalScale).ticks(tlabels.length);
g.call(x_axis).selectAll("line,path").style("stroke", "brown");
}
This drew an axis with the labels I specified, but half of them were piled up on top of each other on the left and the other half on the right. Obviously I have the range wrong but not sure what it wants to be. I tried setting the range to:
.range([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22]);
But this just had them a scrunched up into a black smear on the left.
I spent most of the day looking for examples of what I want to do but the few that looked like they should work, didn't.
TIA for any insights
We're going to try to make the scale a bit more generic, in case you have more data.
Suppose that data is an array of objects, with a date property. The first step is to get the date to an usable state:
function toTitleCase(str) {
return str.replace(/[^-'\s]+/g, function(word) {
return word.replace(/^./, function(first) {
return first.toUpperCase();
});
});
}
var data = data.map(d => toTitleCase(d.date.toLowerCase()))
This will convert every date from "02 JAN 2020" to "02 Jan 2020".
Afterwards, we need to use a date parser. This will convert every date to a real date object:
var parseTime = d3.timeParse("%d %b %Y");
data = data.map(d => parseTime(d.date);
Finally, we can now use a time scale, using d3.extent() to find the min and max value in your dataset. d3.extent() will return an array with two values, the min and the max, and it works on dates, like the rest of D3:
d3
.scaleTime()
.domain(d3.extent(d => d.date))
.nice() // Make the scale look prettier.
That'll give you a scale that works automatically with any date and will work as you add more values.

Is it possible to draw an equation based straight line in D3?

I am new to D3.js, pardon me if my understanding is wrong.
I have an equation for a straight line in a log-log plot, Log(Y)=Log(C) + Log(X), C is constant and user defined.
Is there a way to draw the straight line in D3 purely from the equation?
Thank you.
No this isn't possible exactly as you'd like in D3. D3 is less about mathmatical calculation & visualization compared to other tools (R, MatLab) and is more about binding data sets to DOM and handling animation between data sets.
That being said, if you calculate the X and Y values for the equation then you can plot those values easily. I've seen D3 used like this, with input boxes for C and then plotting across a range.
Following your comment here's an example:
const C = 1;
const xScale = d3.scaleLinear()
.domain([0, 100])
.range([0, 1000]); // pixels
const yScale = d3.scaleLinear()
.domain([0, 100])
.range([0, 1000]);
const line = d3.line()
.x(d => xScale(d))
.y(d => yScale(Math.log(C) + Math.log(d)));
const values = [0, 50, 100];
d3.selectAll("path")
.datum(values)
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("d", line);
Note that the key to pumping in the equation is defining how to generate the y value given the x in the line generator, covered by this line:
.y(d => yScale(Math.log(C) + Math.log(d)))

d3.v4: How to set ticks every Math.PI/2

In the d3.v4 documentation the following is stated:
To generate ticks every fifteen minutes with a time scale, say:
axis.tickArguments([d3.timeMinute.every(15)]);
Is there a similar approach that can be used with values other than time? I am plotting sine and cosine curves, so I'd like the ticks to begin at -2*Math.PI, end at 2*Math.PI, and between these values I'd like a tick to occur every Math.PI/2. I could, of course, explicitly compute the tick values and supply them to the tickValue method; however, if there is a simpler way to accomplish this, as in the time-related example quoted above, I'd prefer to use that.
Setting the end ticks and specifying the precise space of the ticks in a linear scale is a pain in the neck. The reason is that D3 axis generator was created in such a way that the ticks are automatically generated and spaced. So, what is handy for someone who doesn't care too much for customisation can be a nuisance for those that want a precise customisation.
My solution here is a hack: create two scales, one linear scale that you'll use to plot your data, and a second scale, that you'll use only to make the axis and whose values you can set at your will. Here, I choose a scalePoint() for the ordinal scale.
Something like this:
var realScale = d3.scaleLinear()
.range([10,width-10])
.domain([-2*Math.PI, 2*Math.PI]);
var axisScale = d3.scalePoint()
.range([10,width-10])
.domain(["-2 \u03c0", "-1.5 \u03c0", "-\u03c0", "-0.5 \u03c0", "0",
"0.5 \u03c0", "\u03c0", "1.5 \u03c0", "2 \u03c0"]);
Don't mind the \u03c0, that's just π (pi) in Unicode.
Check this demo, hover over the circles to see their positions:
var width = 500,
height = 150;
var data = [-2, -1, 0, 0.5, 1.5];
var realScale = d3.scaleLinear()
.range([10, width - 10])
.domain([-2 * Math.PI, 2 * Math.PI]);
var axisScale = d3.scalePoint()
.range([10, width - 10])
.domain(["-2 \u03c0", "-1.5 \u03c0", "-\u03c0", "-0.5 \u03c0", "0", "0.5 \u03c0", "\u03c0", "1.5 \u03c0", "2 \u03c0"]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var circles = svg.selectAll("circle").data(data)
.enter()
.append("circle")
.attr("r", 8)
.attr("fill", "teal")
.attr("cy", 50)
.attr("cx", function(d) {
return realScale(d * Math.PI)
})
.append("title")
.text(function(d) {
return "this circle is at " + d + " \u03c0"
});
var axis = d3.axisBottom(axisScale);
var gX = svg.append("g")
.attr("transform", "translate(0,100)")
.call(axis);
<script src="https://d3js.org/d3.v4.min.js"></script>
I was able to implement an x axis in units of PI/2, under program control (not manually laid out), by targetting the D3 tickValues and tickFormat methods. The call to tickValues sets the ticks at intervals of PI/2. The call to tickFormat generates appropriate tick labels. You can view the complete code on GitHub:
https://github.com/quantbo/sine_cosine
My solution is to customise tickValues and tickFormat. Only 1 scale is needed, and delegate d3.ticks function to give me the new tickValues that are proportional to Math.PI.
const piChar = String.fromCharCode(960);
const tickFormat = val => {
const piVal = val / Math.PI;
return piVal + piChar;
};
const convertSIToTrig = siDomain => {
const trigMin = siDomain[0] / Math.PI;
const trigMax = siDomain[1] / Math.PI;
return d3.ticks(trigMin, trigMax, 10).map(v => v * Math.PI);
};
const xScale = d3.scaleLinear().domain([-Math.PI * 2, Math.PI * 2]).range([0, 600]);
const xAxis = d3.axisBottom(xScale)
.tickValues(convertSIToTrig(xScale.domain()))
.tickFormat(tickFormat);
This way if your xScale's domain were changed via zoom/pan, the new tickValues are nicely generated with smaller/bigger interval

Logarithmic time scale

How to make a logarithmic datetime scale in D3?
a simple time scale is like this:
d3.time.scale()
.domain([new Date(2014, 0, 1), new Date()])
.range([0, 500])
and a simple log scale is like:
d3.scale.log()
.domain([new Date(2014, 0, 1), new Date()])
.rangeRound([0, 500])
.base(10)
Tried to chain their syntax in a various ways with no effect.
Chart will position users by last login date. Range will be about one year. If we space data linearly, most users will collide during last days/hours. With logarithm we can zoom last hours.
Solution could be by interactive zoom or several charts. But goal here is to make single static chart with nonlinear overview of year.
One alternative could be to convert datetime to "days from now", a number. It would work for data. But then I wouldn't know how to label axis ticks like "01-01-2014"...
Something like the below seems to fool d3js into thinking it has a real scale object. It should make a good starting point:
var xt = d3.scaleUtc()
.domain([start, now])
.range([1, width])
var xp = d3.scalePow()
.exponent(2)
.domain([1, width])
.range([0, width])
// Fool d3js into thinking that it is looking at a scale object.
function x_copy() {
var x = function(t) { return xp(xt(t)) }
x.domain = xt.domain
x.range = xp.range
x.copy = x_copy
x.tickFormat = xt.tickFormat
x.ticks = xt.ticks
return x
}
x = x_copy()
var xAxis = d3.axisBottom(x)
Create two scales and use one after the other. First use the time scale and than the log or pow scale.
var parseDate = d3.time.format("%Y-%m-%d").parse;
var x = d3.time.scale()
.range([0,width]);
var xLog = d3.scale.pow().exponent(4)
.domain([1,width])
.range([0,width]);
than I'm using .forEach to get the linear points:
x.domain(d3.extent(data, function(d) { return parseDate(d.start); }));
data.forEach(function(d) {
d.start = x(parseDate(d.start));
});
when I'm drawing the objects I add the log scale:
.attr('cx', function (d) { return xLog(d.start)})

domain / padding in d3.js linear scales

Folks-
I've been trying to set the d3.scale.linear.domain x axis value manually, using Mike Bostock's simple bar chart: http://bl.ocks.org/mbostock/raw/2368837/
var x = d3.scale.linear()
.domain([-100, 200]) //this line added
.range([0, width])
I would expect this to yield a an x axis with values -100 to 200. Instead it yields -30 to 10, as before, and the chart doesn't change. Any ideas?
Thanks,
RL
The domain is set again inside the d3.tsv callback:
x.domain(d3.extent(data, function(d) { return d.value; })).nice();
If you want to change it, you need to delete that line.

Resources