Trying to modify the code at the below link to create an animated scatter which shows data points moving over time.
http://bost.ocks.org/mike/nations/
Struggling to wrap my head around the data interpolation section
// Interpolates the dataset for the given (fractional) year.
function interpolateData(year) {
return nations.map(function(d) {
return {
employee: d.employee,
category: d.category,
x:interpolateValues(d.x, year),
y:interpolateValues(d.y, year)
};
});
}
// Finds (and possibly interpolates) the value for the specified year.
function interpolateValues(values, year) {
var i = bisect.left(values, year, 0, values.length - 1),
a = values[i];
if (i > 0) {
var b = values[i - 1],
t = (year - a[0]) / (b[0] - a[0]);
return a[1] * (1 - t) + b[1] * t;
}
return a[1];
}
});
Code is expecting the data in this format, with the x and y values containing an array with the corresponding year and value.
d: Object
category: "1"
employee: "12017512"
x: Array[63]
y: Array[63]
The data I'm passing in is in this format, with a record for each each year.
d: Object
category: "1"
employee: "12017512"
x: 2697.3199999999993
y: 24
year: "2015"
How do I modify the code to accept the data in the format I have?
Managed to solve my issue. The answer if its helps anyone was to use underscore.js and the following code.
I also found the following jsfiddle helpful.
http://jsfiddle.net/bgAzH/1/
groupData = _.map(_.groupBy(data, 'dimension1', 'dimension2'), function (b) {
return _.extend(_.pick(b[0], 'dimension1', 'dimension2'), {
x: _.map(b, function (elem) {
return _.pick(elem, 'time', 'x')
}),
y: _.map(b, function (elem) {
return _.pick(elem, 'time', 'y')
})
});
});
Then when passing the array of objects into the interpolate values functions I converted the objects to simple arrays.
var values = values.map(function (obj) { return [Number(obj.time), obj[element]] });
Related
I am using dc to create a line graph where capacity is on the y-axis and week is on the x-axis. For weeks, the range is 1-52, but there is no data from weeks 2-40. I only have data for week 1 and 41-52, but my line graph is still creating a line when there is no data:
How do I get it so the line graph will break if there are no values? So it wouldn't be one connected line. Here is my code for reference
let chart = dc.lineChart("#chart");
let ndx = crossfilter(results);
let weekDimension = ndx.dimension(function (d) {
return d.week = +d.week;
});
function reduceAdd(p, v) {
++p.count;
p.total += v.capacity;
p.average = p.total / p.count;
return p;
}
function reduceRemove(p, v) {
--p.count;
p.total -= v.capacity;
p.average = p.count ? p.total / p.count : 0;
return p;
}
function reduceInitial() {
return { count: 0, total: 0, average: 0 };
}
let capacityGroup = weekDimension.group().reduce(reduceAdd, reduceRemove, reduceInitial);
chart.width(360)
.height(200)
.margins({ top: 20, right: 20, bottom: 50, left: 30 })
.mouseZoomable(false)
.x(d3.scale.linear().domain([1, 52]))
.renderHorizontalGridLines(true)
.brushOn(false)
.dimension(weekDimension)
.valueAccessor(function (d) {
return d.value.average;
})
.group(capacityGroup);
dc.renderAll('chart');
This is how results would look like
{month : "1", capacity: "48"}
{month : "1", capacity: "60"}
{month : "42", capacity: "67"}
{month : "42", capacity: "60"}
{month : "43", capacity: "66"}
{month : "44", capacity: "52"}
{month : "45", capacity: "63"}
{month : "46", capacity: "67"}
{month : "47", capacity: "80"}
{month : "48", capacity: "61"}
{month : "48", capacity: "66"}
{month : "49", capacity: "54"}
{month : "50", capacity: "69"}
I have tried to add .defined(d => { return d.y != null; }); and .defined(d => !isNaN(d.value)); but that didn't do anything... Any help will be greatly appreciated
As we discussed in the comments, the important problem is that dc.js will only draw the data it receives. It doesn't know if data is missing, so we will need to fill in the nulls in order to draw gaps in the line.
I linked to a previous question, where the data is timestamps. The answer there uses a d3 time interval to generate the missing timestamps.
However, your data uses integers for keys (even though it represents weeks), so we will need to change the function a little bit:
function fill_ints(group, fillval, stride = 1) { // 1
return {
all: function() {
var orig = group.all();
var target = d3.range(orig[0].key, orig[orig.length-1].key, stride); // 2
var result = [];
for(var oi = 0, ti = 0; oi < orig.length && ti < target.length;) {
if(orig[oi].key <= target[ti]) {
result.push(orig[oi]);
if(orig[oi++].key === target[ti])
++ti;
} else {
result.push({key: target[ti], value: fillval});
++ti;
}
} // 3
if(oi<orig.length) // 4
Array.prototype.push.apply(result, orig.slice(oi));
if(ti<target.length) // 5
result = [...result, ...target.slice(ti).map(t => ({key: t, value: fillval}))];
return result;
}
};
}
This function takes a group, the value to fill, and a stride, i.e. the desired gap between entries.
It reads the current data, and generates the desired keys using d3.range.
It walks both arrays, adding any missing entries to a copy of the group data.
If there are any leftover entries from the original group data, it appends it.
If there are any remaining targets, it generates those.
Now we wrap our original group using this function, creating a "fake group":
const filledGroup = fill_ints(capacityGroup, {average: null});
and pass it to the chart instead:
.group(filledGroup);
One weakness of using LineChart.defined(), and the underlying d3.line.defined, is that it takes two points to make a line. If you have isolated points, as week 1 is isolated in your original data, then it won't be shown at all.
In this demo fiddle, I have avoided the problem by adding data for week 2.
But what about isolated dots?
I was curious how to solve the "isolated dots problem" so I tried showing the built-in dots that are usually used for a mouseover effect:
chart.on('pretransition', chart => {
const all = chart.group().all();
isolated = all.reduce((p, kv, i) => {
return (kv.value.average !== null &&
(i==0 || all[i-1].value.average == null) &&
((i==all.length-1 || all[i+1].value.average == null))) ?
{...p, [kv.key]: true} : p;
}, {});
chart.g().selectAll('circle.dot')
.filter(d => isolated[d.data.key])
.style('fill-opacity', 0.75)
.on('mousemove mouseout', null)
})
This works but it currently relies on disabling the interactivity of those dots so they don't disappear.
How could I write a function that takes a value and produce a list of values?
Ex: value = 2, function would increment by 1 until a condition is reached (or infinite and I would just take/drop from it), and expected result should look like: [3,4,5,6,7,8...]
right now I'm doing this ugly thing:
let min = ...;
const max = ....;
const acc = [min];
while (min.plus(1). < max) {
min = min.plus(1);
acc.push(min);
}
bonus question .... how could I represent this in type notation? Would it be: (a -> [a]) -> a -> [a]?
Thanks!
function recursivePlus(min, max, collection) {
if (!collection) {
collection = [];
collection.push(min);
return recursivePlus(++min, max, collection);
} else {
if (min <= max) {
collection.push(min);
return recursivePlus(++min, max, collection);
} else {
return collection;
}
}
}
recursivePlus(0, 3);
Maybe use lodash range https://lodash.com/docs/4.17.11#range
_.range(2, 9);
// => [2, 3, 4, 5, 6, 7, 8]
or use Array.apply (dense array) way:
How to generate sequence of numbers/chars in javascript?
https://2ality.com/2012/06/dense-arrays.html
I think range of dates as timestamps could be created with
const startDate = new Date("2019-10-16");
const endDate = new Date("2019-10-18");
const dayInMillis = 24 * 60 * 60 * 1000;
_.range(startDate.getTime(), endDate.getTime() + 1, dayInMillis);
// => [1571184000000, 1571270400000, 1571356800000]
I found the solution I was looking for! Namely: unfold, it does the reverse of fold/reduce it would be something like this (Mind that I'm using Ramda.js a js functional library similar to underscore and lodash):
const f = n =>
n.toMillis() > maxDate.toMillis() ? false : [n, n.plus({ days: 1 })]
unfold(f, minDate);
Thanks for trying to help me out guys, appreciated.
PS: I'll leave this article just in case someone is interested: https://kseo.github.io/posts/2016-12-12-unfold-and-fold.html
I've got a problem sorting arrays. I'm currently trying to optimize a thing in a strategy game I play, and for that I need to calculate distance between all members of my alliance, the first towards the others and so on. No problem doing that actually. But now, what I want to do is sort the array of distance "ascending" and problem is, I need to write the corresponding nickname to match the distance. I've been searching for 2 days and I can't figure out a working solution.
I tried to copy the array before sorting it, but I need the unsorted array and with that sort function, it sorts the copy too !
Actually the code provided is good, speaking of distance accuracy but not sorted ascending. If I sort the distances, the nicknames are no longer corresponding. I don't know why they appear in the order of the pseudo_list because It's supposed to be sorted through nSort2()
This is what I've ended up with so far :
//Sorting Distance[i] Array List
function nSort(arr)
{
return arr.sort((a, b) => a - b);
}
//Calculating Distance
function calcDist(xA, yA, xB, yB)
{
return Math.sqrt(Math.pow((xB-xA), 2)+Math.pow((yB-yA), 2));
}
//Here i'm trying to retrieved unsorted position of distance by index to sort the nicknames by their respective distances
function nSort2(arr_str, arr_nbr)
{
var arr_nbr2 = arr_nbr.splice(0);
var arr_sort = nSort(arr_nbr2);
var str_sort = [];
arr_str.forEach(function(element, i)
{
j = arr_sort.indexOf(arr_nbr2[i], i);
str_sort[i] = arr_str[j];
});
console.log(str_sort);
return str_sort;
}
var pseudo_list = ["teddy95", "gabrielc", "ngozi"]; //The list (I just put the first 3 to not to write to much unnecessary code)
var x_ = [29, 26, 4]; // The X Coordinate list
var y_ = [519, 461, 143]; // The Y Coordinate list
var distance = [[]]; // The 2D Array for distance (distance[0][0] being the member's distance tower himself (which is obviously 0).
//Calculating Distances And Storing them in the 2D Array
y_.forEach(function(element, i)
{
distance[i] = [];
x_.forEach(function(element, j)
{
distance[i][j] = Math.ceil(calcDist(x_[i], y_[i], x_[j], y_[j]));
});
});
//Displaying Sorted Array ascending (Trying)
y_.forEach(function(element, i)
{
x_.forEach(function(element, j)
{
document.write(pseudo_list[i] + ' -> ' + nSort2(pseudo_list, distance[i])[j] + ': ' + distance[i][j] + '<br>');
});
});
I think your problem come from over complicating the data structures (I'm not insulting you just sharing an opinion).
In the code below all the input (pseudo, x, y) is stored in an object so player data is easier to manipulate.
Then I'm not using a matrix because you end up creating new issues namely I'd expect distance[1][2] = distance[2][1] so sorting will create duplicate results (and the diagonal doesn't help since it represents the distance from yourself). Instead I have a 1D array constructed with no duplicates, i.e. it contains the distance from the first element to all the others (i.e. second, third, ...), then the second element from the "ones on the right" (i.e. third, fourth, ...), ...
Once you have all the distance information, sorting is a trivial task so is displaying the result.
//Calculating Distance
function calcDist(xA, yA, xB, yB) {
return Math.sqrt(Math.pow((xB - xA), 2) + Math.pow((yB - yA), 2));
}
let players = [{
pseudo: "teddy95",
x: 29,
y: 519
},
{
pseudo: "gabrielc",
x: 26,
y: 461
},
{
pseudo: "ngozi",
x: 4,
y: 143
}]
let distances = []
players.forEach(function (element, i) {
for (let j = i + 1; j < players.length; ++j) {
distances.push({
player1: element,
player2: players[j],
distance: Math.ceil(calcDist(element.x, element.y, players[j].x, players[j].y))
})
}
})
distances.sort(function (a, b) { return a.distance - b.distance })
distances.forEach(function (element, i) {
document.write(element.player1.pseudo + ' - ' + element.player2.pseudo + ' dist ' + element.distance + '<br>')
})
I'm trying to use mapreduce to calculate monthly sales and graph with chart.js later.
Suppose I have a JSON response with fields, amount and date.
For my map function, I took out the month:
month = _.map([items[i].transactionDate], function(date) {
return {
lables: items[i].transactionDate.slice(5,7)
};
});
and for my reduce function ... well I didn't know what to do here.
sum = _.reduce([items[i].amt], function(memo, num) {
return memo + items[i].amt
});
I realized that this will eventually calculate total sum not per month. Problem is that I don't know how to relate the two functions properly.
Edit: Per request, my JSON :
"items": [
{
"_id": "56d1cf3704aee3c68d89cc09",
"amt": 5,
"transactionDate": "2016-02-27T16:30:47.561Z",
}
]
and what I'm trying to get out of this is sales per month on a graph. so far I've been able to project months and total sale but not sales per a specific month.
I think, simply make one loop with adding new variable
let result= {};
_.forEach(items, (item) => {
// get month
let month = item.transactionDate.slice(5,7);
// get amount
let amount = item.amt;
// check if we have this month in finaly object
if(month in finaly) {
// added amount
result[month] += amount;
} else {
// if this month doesn't exist added with inital value
result[month ] = amount;
}
});
When you can get all amount of certain month or get sum of all months
let allSum = _.reduce(result, (sum, amount) => sum += amount);
let amountInCertainMonth = result["01"];
map and reduce are functions of Array that iterate over the array for you
map returns a new array with the results of the mapping function for each element
var months = items.map ( item => item.transactionDate.slice(5,7) )
and reduce applies the reducing function to each element and an accumulator.
var sum = items.reduce( (accum, item) => accum + item.amt , 0);
I assume items[i] is an object that has .transactionsDate and .amt as two arrays with corresponding indices. If so, you can get sales per month and total sales within one reduce function by starting with { totalSales, months }.
var chartData = items[i].transactionDate.reduce(function(total, month, index) {
var monthlyData = {
label: month.slice(5, 7),
sales: items[i].amt[index]
};
total.months.push(monthlyData);
total.totalSales += item.amt;
return total;
}, {
totalSales: 0, months: []
});
// get total sales
var totalSales = chartData.totalSales;
// get sales for May starting at index 0
var salesForMay = chartdata.months[4];
Let me know if this is what you were looking for.
I know this is old, but here is how to use reduce as a group by
var results = items
.map(function(data){
return {"month": data.transactionDate.slice(0,7), "amt": data.amt};
})
.reduce(function(amounts, data){
if (!amounts.hasOwnProperty(data.month))
amounts[data.month] = data.amt;
else
amounts[data.month] = amounts[data.month] + data.amt;
return amounts;
}, {});
I need to create the following array dynamically, for example:
var data = { point: [
{ x:5, y:8 },
{ x:8, y:10},
]};
console.log(data.point[0].x); // 5 n=0
console.log(data.point[1].y); // 10 n=1
At some point my application needs to expand the array to more than 2 items (n=0, n=1). Please let me know how to do that (i.e. n = 9 ).
You could use Array.push method to add element to an array.
var point = {x:1,y:1};
data.point.push(point);
you can use method 'push' like this code
var data = { point: [
{ x:5, y:8 },
{ x:8, y:10},
]};
console.log(data.point[0].x); // 5 n=0
console.log(data.point[1].y); // 10 n=1
data.point.push({x:4,y:3});
console.log(JSON.stringify(data.point));
You could do something like this:
var data = {'points' : []};
function addPoint(x, y) {
data.points.push({'x' : x, 'y' : y});
}
function getPoint(index) {
return data.points[index];
}
addPoint(10, 20);
addPoint(5, 3);
var p = getPoint(1);
alert(p.x + ", " + p.y); //alerts => "5, 3"
This would work for any arbitrary number of points.
Update
To clear the array
function clearPoints() {
data.points = [];
}
Update 2
These little functions will work okay if you have a simple page. If this points handling is going to end up being part of a larger system, it may be better to do something like this:
var data = {
'points' : [],
addPoint : function(x, y) {
this.points.push({
'x' : x,
'y' : y
});
},
getPoint : function(index) {
return this.points[index];
},
clearPoints : function() {
this.points = [];
},
removePoint : function(index) {
this.points.splice(index, 1);
}
};
Example usage:
alert(data.points.length); // => 0
data.addPoint(1, 2);
data.addPoint(8, 12);
data.addPoint(3, 7);
alert(data.points.length); // => 3
var p = data.getPoint(2); //p = {x:3, y:7};
data.removePoint(1);
alert(data.points.length); // => 2
data.clearPoints();
alert(data.points.length); // => 0
It may allow you to keep your point handling a little cleaner and easier to use and update.
You can either use the push() function as stated, or add additional items to the array using an index, which is preferred in the Google Style Guide. The latter method does require that you have a pointer to the last index.
Note that since assigning values to an array is faster than using
push() you should use assignment where possible.
for(var i=lastIndex; i < numOfNewPoints; i++){
data.point[i] = {x:4, y:3};
}