4
\$\begingroup\$

I have some code that does what I want - it just doesn't feel correct for being done in JavaScript.

What it does is take data that is like this:

 var data = [['Marker', 'sampleName1', 'sampleName2'],
 ['CCR4', 71.6, 83.4],
 ['CD27', 42.3, 76.2]];

And create a list of objects that looks like this (to use the plot.ly JavaScript library):

[{x:['CCR4', 'CD27'], y:[71.6,42.3], name: 'sampleName1'},
{x:['CCR4', 'CD27'], y:[83.4,76.2], name: 'sampleName2'}]

What I have is this:

var bData = [];
for (var i=0; i < data.length; i++) {
 var row = data[i];
 if (i ==0) {
 //Handle creation of objects
 for (var k = 1; k < row.length; k++) {
 var obj = {name : row[k], type:'bar', x:[], y:[]};
 bData.push(obj);
 }
 } else {
 var marker = row[0];
 for (var k=1; k < row.length; k++) {
 var obj = bData[k -1];
 obj.x.push(marker);
 obj.y.push(row[k]);
 }
 }
}

What would be a better, more legible approach to this?

Jamal
35.2k13 gold badges134 silver badges238 bronze badges
asked Nov 20, 2015 at 22:42
\$\endgroup\$
0

4 Answers 4

3
\$\begingroup\$

Here's a functional solution which I think is cleaner and makes what you are actually doing (essentially extracting columns from the data) more clear.

I put the explanation inline as comments:

function plotlyFormat(data) {
 var data = data.slice(0), // make a copy
 headers = data.shift(), // remove header row and save it
 x = column(data, 0); // calculate the x property, same for all objects
 // Create the objects, each one's "y" data is just a column 
 // of the original data
 return headers.map(function(name, i) { 
 return {x: x, y: column(data,i), name: name}
 }).slice(1); 
 // ^^^ the final "slice(1) above removes the first record, 
 // since it's the "Marker" column, not an actual sample
 // simple utility function to extract a column from a nested array (matrix)
 function column(matrix, i) { return matrix.map(function(row) { return row[i] }) }
}

Just call the function on your input data, and it will return it in the format you want.

And an uncommented version, to see the character savings :)

function plotlyFormat(data) {
 var data = data.slice(0),
 headers = data.shift(),
 x = column(data, 0);
 return headers.map(function(name, i) { 
 return {x: x, y: column(data,i), name: name}
 }).slice(1);
 function column(matrix, i) { return matrix.map(function(row) { return row[i] }) }
}
answered Nov 20, 2015 at 23:32
\$\endgroup\$
3
\$\begingroup\$

Although I like Johna's solution (you, too, as it seems, the green hook came up just when I was writing this ;-), it might be a bit too modern for some browsers.

But let's analyze your problem first:

The example you have given can be converted with the following code (no checks, no balances, nothing. Of course.)

var obj = {};
obj.x = [data[1][0],data[2][0]];
obj.y = [data[1][1],data[2][1]];
obj.name = data[0][1];
bData.push(obj);
obj = {};
obj.x = [data[1][0],data[2][0]];
obj.y = [data[1][2],data[2][2]];
obj.name = data[0][2];
bData.push(obj);

Now look for repetitions and regularly in/decreasing values.

obj.x is always the same, build it once and use it often.

obj.y has two increasing values. They have the same starting point and the same increment.

obj.name has one increasing value of the same starting point and increment as obj.y but it is an independant iterator, we need another loop here.

What happens if we add another sample?

var data = [['Marker', 'sampleName1', 'sampleName2', 'sampleName3'],
 ['CCR4', 71.6, 83.4, 23.3],
 ['CD27', 42.3, 76.2, 34.4],
 ['CK56', 102.3, 6.2, 786.4]];

The unrolled loop would be

var obj = {};
obj.x = [data[1][0],data[2][0],data[3][0]];
obj.y = [data[1][1],data[2][1],data[3][1]];
obj.name = data[0][1];
bData.push(obj);
obj = {};
obj.x = [data[1][0],data[2][0],data[3][0]];
obj.y = [data[1][2],data[2][2],data[3][2]];
obj.name = data[0][2];
bData.push(obj);
obj = {};
obj.x = [data[1][0],data[2][0],data[3][0]];
obj.y = [data[1][3],data[2][3],data[3][3]];
obj.name = data[0][3];
bData.push(obj);

Nothing has changed in regards to the rules; induction works in programming, too (sometimes, not always; rarely even, if at all). That is easily translated into three loops. The complexity is nevertheless still O(n^2) because the data for the third loop does not get used in the main loops.

// temporary variable for the keys
var keys = [];
// gather the keys
for(var i=1;i<data.length;i++)
 keys.push(data[i][0]);
// loop over the whole array, start at 1 (one)
// because the zeroth row has the sample names in it
// and no actual data-points
for(var i = 1;i < data.length;i++){
 // you said, you need proper JavaScript objects
 var obj = {};
 // add the data keys. We do a full copy here
 // which is probably unnecessary
 obj.x = keys.slice(0);
 // temporary variable for the data-points
 var points = [];
 // gather the correct column for the data-points
 for(var j=1;j<data[0].length;j++)
 points.push(data[j][i]);
 // full copy, again. And again: probably unnecessary
 obj.y = points.slice(0);
 // put the correct sample name on it
 obj.name = data[0][i];
 // you want all of the objects in an array
 // and so be it
 bData.push(obj);
}

Putting that into a function is beyond the intend of this post ;-)

answered Nov 21, 2015 at 0:01
\$\endgroup\$
1
\$\begingroup\$

Notice something in this code:

for (var i=0; i < data.length; i++) {
 var row = data[i];
 if (i ==0) {
 // ...
 } else {
 // ...
 }
}

The condition i == 0 inside the loop will only be true in the first iteration. So no need to have that statement inside the loop. It would be better to do that before the loop, and change the loop to start the iteration from 1 instead of 0.

Something like this:

var bData = [];
var row = data[0]; // note: you might want to check first if data is empty
for (var k = 1; k < row.length; k++) {
 var obj = {name: row[k], type: 'bar', x:[], y:[]};
 bData.push(obj);
}
for (var i = 1; i < data.length; i++) {
 var row = data[i];
 var marker = row[0];
 for (var k = 1; k < row.length; k++) {
 var obj = bData[k -1];
 obj.x.push(marker);
 obj.y.push(row[k]);
 }
}
answered Nov 20, 2015 at 23:21
\$\endgroup\$
0
\$\begingroup\$

To reach a high level of expandability, I came up with this:

function defaultFor(arg, val) {
 return (typeof arg !== "undefined") ? arg : val;
}
Marker = function(name, x, y) {
 this.name = defaultFor(name, "");
 this.x = defaultFor(x, []);
 this.y = defaultFor(y, []);
};
function createMarkers(data) {
 if (data.length <= 0) return [];
 var names = data.shift();
 if (names.length <= 0) return [];
 if (names.shift() !== "Marker") return [];
 var markers = [];
 var xValues = [];
 for (var i in names) {
 var marker = new Marker(names[i]);
 markers.push(marker);
 var markerData = data.shift();
 if (typeof markerData === "undefined") break;
 if (markerData.length != 3) break;
 xValues.push(markerData.shift());
 marker.y = markerData;
 }
 for (var j in markers) {
 markers[j].x = xValues;
 }
 return markers;
}
var data = [
 ['Marker', 'sampleName1', 'sampleName2'],
 ['CCR4', 71.6, 83.4],
 ['CD27', 42.3, 76.2]
];
var bData = createMarkers(data);

I make use of plain old JS objects (POJOs) to centralize object creation (so that it can be re-used), and Array.shift() to de-obfuscate the meanings behind index arithmetics. We need less loops and code that explains what it does.

You will also notice my usage of early returns in favor of defensive programming. The best example is if (markerData.length != 3) break; because it clearly states those assumptions we make about the data that are initially unclear to any reader.

answered Nov 20, 2015 at 23:30
\$\endgroup\$
2
  • 1
    \$\begingroup\$ Please don't rewrite and give a two-paragraph description; prefer explaining point-by-point what changes you made and why. \$\endgroup\$ Commented Nov 20, 2015 at 23:52
  • \$\begingroup\$ This answer is fine, in my opinion. Although the explanation is brief, there is explanation, providing guidance the OP can use. \$\endgroup\$ Commented Nov 21, 2015 at 19:30

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.