Creating unique scales for parallel coordinates using d3 (multivariate) - d3.js

I'm trying to find a way to assign different scale max/min to different columns of my data using parallel coordinates. I've adopted code from http://bl.ocks.org/jasondavies/1341281 but do not want to use d3.extent. I've tried different methods (for loops, if statements) but d3 doesn't seem to like that.
Any ideas for how I should approach this?
d3.json("HW3/scores.json", function(error, scores){
x.domain(dimensions = d3.keys(scores[0]).filter(function(d){
return d != "Country" && (y[d] = d3.scale.linear()
.domain(d3.extent(scores, function(p) { return +p[d]; }))
.range([height, 0]));
}));
I'm trying to change the domains of 6 scales (pulled from scores.json, all numbers in column format) to be unique to the data type:
percent (columns 1 and 6) should be from 1 - 100,
score (columns 3 and 4) should be from 200 - max of both score
columns,
hours (columns 2 and 5) should be from 0 - max of both hour columns.
Please and Thank You for ANY help you can provide to a d3 n00b.

You can simply remove the code that creates the scales from the filter function and create the y scales manually according to your specification:
x.domain(dimensions = d3.keys(data[0]).filter(function(d) {
return d != "name";
}));
y['percent 1'] = d3.scale.linear()
.domain([1, 100])
.range([height, 0]));
y['hours 1'] = d3.scale.linear()
.domain([0, d3.max(data, function(p) { return Math.max(+p['hours 1'], +p['hours 2']); }])
.range([height, 0]));
// etc

Related

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

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.

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)})

d3 mapping numbers with scale

If I set scales so points are in scale on the svg with:
d3.scale.linear().domain(xDomain).range([0, width]);
How can I get the unscaled number of the mouse x position?
Eg.
xPositions = [1,7,10]
7 is # x = ~300 on an svg of width 500
How would I map 300 to 7 based on arbitrary data?
You can use the scale.invert function for the inverse mapping:
var xScale = d3.scale.linear().domain(xDomain).range([0, width]);
var xDomainPos = [1, 7, 10];
var xRangePos = xDomainPos.map(function (d) { return xScale(d); });
var xNewDomainPos = xRangePos.map(function (d) { return xScale.invert(d); });

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.

Why is domain not using d3.max(data) in D3?

I'm new to D3 and playing around with a scatterplot. I cannot get d3.max(data) to work correctly in setting up domain!
I have the following setting up a random dataset:
var data = [];
for (i=0; i < 40; i++){
data.push({"x": i/40, "y": i/8, "a": Math.floor(Math.random() * 3), "x2": Math.random()});
}
And then the following to set my coordinates:
var x = d3.scale.linear().domain([0, 1]).range([0 + margin, w-margin]),
y = d3.scale.linear().domain([0, d3.max(data)]).range([0 + margin, h-margin]),
c = d3.scale.linear().domain([0, 3]).range(["hsl(100,50%,50%)", "rgb(350, 50%, 50%)"]).interpolate(d3.interpolateHsl);
This puts all 40 points in a single, horizontal line. If I replace d3.max(data) with '5' then it is a diagonal (albeit from the upper left to the bottom right, I'm still struggling to flip y-coordinates). Why isn't d3.max(data) working as expected?
d3.max() expects an array of numbers, not of objects. The elements of data have an internal key-value structure and there is no way for d3.max() to know what to take the maximum of. You can use something like jQuery's $.map to get the elements of the objects you want and then take the max, e.g.
var maxy = d3.max($.map(data, function(d) { return d.y; }));
Edit:
As pointed out in the comment below, you don't even need JQuery for this, as .map() is a native Array method. The code then becomes simply
var maxy = d3.max(data.map(function(d) { return d.y; }));
or even simpler (and for those browsers that don't implement Array.map()), using the optional second argument of d3.max that tells it how to access values within the array
var maxy = d3.max(data, function(d) { return d.y; });
d3.max API documentation can be found here.
# d3.max(array[, accessor])
Returns the maximum value in the given array using natural order. If
the array is empty, returns undefined. An optional accessor function
may be specified, which is equivalent to calling array.map(accessor)
before computing the maximum value. Unlike the built-in Math.max, this
method ignores undefined values; this is useful for computing the
domain of a scale while only considering the defined region of the
data. In addition, elements are compared using natural order rather
than numeric order. For example, the maximum of ["20", "3"] is "3",
while the maximum of [20, 3] is 20.
Applying this information to the original question we get:
function accessor(o){
return o.y;
}
var y = d3.scale.linear()
.domain([0, d3.max(data, accessor)])
.range([0 + margin, h-margin]);
If you end up using many accessor functions you can just make a factory.
function accessor(key) {
return function (o) {
return o[key];
};
}
var x = d3.scale.linear()
.domain([0, d3.max(data, accessor('x'))])
.range([...]),
y = d3.scale.linear()
.domain([0, d3.max(data, accessor('y'))])
.range([...]);
I was having a similar issue dealing with an associative array. My data looked like the following: [{"year_decided":1982,"total":0},{"year_decided":"1983","Total":"847"},...}]
Simply passing parseInt before returning the value worked.
var yScale = d3.scale.linear()
.domain([0, d3.max(query,function(d){ return parseInt(d["Total"]); }) ])
.range([0,h]);

Resources