The docs for d3's stacking function d3.stack show an example with an array of objects (each json object representing the ensemble of points for whatever the x-axis is measuring). Eg:
var data = [
{month: new Date(2015, 0, 1), apples: 3840, bananas: 1920, cherries: 960},
{month: new Date(2015, 1, 1), apples: 1600, bananas: 1440, cherries: 720}
]
I'm trying to produce a stacked histogram with a matrix of data series ([ [], [], [], etc ]). It's easy enough to iterate through the rows and get a series of histogram bins (having pre-defined the x scale and domain elsewhere):
for(let i=0; i<data.length; i++){
bins[i] = d3.histogram()
.domain(x.domain())
.thresholds(x.ticks(10))
(data[i]);
}
And create groups for each data series inside another loop:
let bars = this.svg.selectAll(".series" + i)
.data(this.bins[i])
.enter().append("g")
.classed("series" + i, true)
But of course doing it like that I get stuck here. How am I supposed to bars.append("rect") at the correct x,y coords for that particular series? Stated differently, I have a really useful array of bins at the moment, looking something like:
[
[[1,2,3,3], [5,8,9], [10], ... etc], //series0 grouping by bins of 5
[[1,3], [7,7,9,9], [11], ... etc], //series1
[[2,3,3], [8,9], [10,12], ... etc], //series2
...etc
]
Is there a way to invoke stack without munging all the data into json key,value pairs?
I took a glance at the source and no comments + single char variables = me understanding that it's not going to happen without munging. I present therefore my shoddy attempt at saving someone else some time:
/*
* Static helper method to transform an array of histogram bins into an array of objects
* suitable for feeding into the d3.stack() function.
* Args:
* bins (array): an array of d3 histogram bins
*/
static processBins(bins){
let temp = {}; // the keys for temp will be the bin name (i.e. the bin delimiter value)
// now create an object with a key for each bin, and an empty object as a placeholder for the data
bins[0].map( (bin) => { temp[bin.x0] = {}});
for(let i=0; i<bins.length; i++){
//traverse each series
bins[i].map( bin => {
temp[bin.x0]["series"+i] = bin.length; //push the frequency counts for each series
});
}
/* now we have an object whose top-level keys are the bins:
{
binName0: { series0: freqCount0, series1: freqCount1, ...},
binName1: {...},
...
}
now, finally we're going to make an arrays of objects containing all the series' freqencies for that bin
*/
let result = [];
for(let binName in temp){ // iterate through the bin objects
let resultRow = {};
if(temp.hasOwnProperty(binName)){
resultRow["bin"] = binName; //put the bin name key/value pair into the result row
for(let seriesName in temp[binName]){ //iterate through the series keys
if(temp[binName].hasOwnProperty([seriesName])){
resultRow[seriesName] = temp[binName][seriesName];
}
}
}
result.push(resultRow);
}
return result;
}
Call like:
let stack = d3.stack().keys( bins.map( (d,i)=>{return "series"+i})); //stack based on series name keys
let layers = stack(MyCoolHistogram.processBins(bins));
//and now your layers are ready to enter() into a d3 selection.
Edit:
I note that the stack data third argument in anonymous functions seems to be the array of elements. I.e. it's no longer the stack layer index. Eg, when grouping bars side-by-side: http://bl.ocks.org/mbostock/3943967
This breaks grouping functions that rely on this index number to calculate the x position:
rect.attr("x", (d,i,j) => { return x(d.data.bin) + j*barWidth/numberOfSeries});
I guess it's telling that Mike's gist still uses v3, despite being updated long after v4 came out.
To get the layer index you have to use the layer.index attribute directly. So when grouping you would translate the entire layer (which screws up bar-by-bar animations, of course... sigh).
let layers = d3.stack(yourData);
let layer = this.svg.selectAll(".layer")
.data(layers)
layer.transition()
.attr("transform", d => { return "translate(" + d.index*barWidth/numberOfSeries + ",0)"; });
Related
Is there any standard way or algorithm for below transformation of matrix in relative position in the space considering x=0 and y=0as origin and downward y-axis and rightward x-axis as positive axes.
[ [{x:36,y:14},{x:242,y:14}],
[{x:36,y:133}],
[{x:36,y:252}],
[{x:36,y:371},{x:242,y:371},{x:446,y:371},{x:651,y:371}],
[{x:242,y:490},{x:446,y:490},{x:651,y:490}] ]
Now because the length of this array of arrays is 5 and length of longest array within it is 4, I need transformed matrix of size 5 * 4 in below format.
[ [{x:36,y:14},{x:242,y:14},null,null],
[{x:36,y:133},null,null,null],
[{x:36,y:252},null,null,null],
[{x:36,y:371},{x:242,y:371},{x:446,y:371},{x:651,y:371}],
[null,{x:242,y:490},{x:446,y:490},{x:651,y:490}] ]
In the above case there relative positions are preserved.
Thanks in advance!!
Solution reduces out all the unique x values into sorted flat array first.
Then loop over each row of data and go through each row array splicing null into the holes
let data =[ [{x:36,y:14},{x:242,y:214}],
[{x:36,y:133}],
[{x:36,y:252}],
[{x:36,y:371},{x:242,y:371},{x:446,y:371},{x:651,y:371}],
[{x:242,y:490},{x:446,y:490},{x:651,y:490}] ]
let xVals = [...new Set(data.reduce((a,c)=>a.concat(c.map(({x})=>x)),[]))].sort((a,b)=>a-b)
data.forEach(row=>{
xVals.forEach((x,i)=>{
if(row[i] === undefined || row[i].x > x){
row.splice(i,0, null)
}
});
});
data.forEach(arr=>console.log(JSON.stringify(arr)))
Check at this piece of code. Explanation will be commented there.
function normalize(array){
// Get the largest sub-array. We will save this as a reference
// to use it later
var longest_value = array.reduce((a,b)=>a>b?a:b)
// map each element in the main array
return array.map(function(a){
// for each item return a modified copy of the largest one.
// To do this we map it
return longest_value.map(function(b,i){
// we the item with the same x position in the current main array item
var v = a.filter(r=>r.x==b.x)
//if there is, we return it, is not we return null
return v.length? v[0] : null
})
})
}
console.log(normalize([ [{x:36,y:14},{x:242,y:214}],[{x:36,y:133}],[{x:36,y:252}],[{x:36,y:371},{x:242,y:371},{x:446,y:371},{x:651,y:371}],[{x:242,y:490},{x:446,y:490},{x:651,y:490}] ]))
We have scatter plots working great in our dashboard, but we have been thrown a curve ball. We have a new dataset that provides multiple y values for a single key. We have other datasets were this occurs but we had flatten the data first, but we do not want to flatten this dataset.
The scatter plot should us the uid for the x-axis and each value in the inj field for the y-axis values. The inj field will always be an array of numbers, but each row could have 1 .. n values in the array.
var data = [
{"uid":1, "actions": {"inj":[2,4,10], "img":[10,15,25], "res":[15,19,37]},
{"uid":2, "actions": {"inj":[5,8,15], "img":[5,8,12], "res":[33, 45,57]}
{"uid":3, "actions": {"inj":[9], "img":[2], "res":[29]}
];
We can define the dimension and group to plot the first value from the inj field.
var ndx = crossfilter(data);
var spDim = ndx.dimension(function(d){ return [d.uid, d.actions.inj[0]];});
var spGrp = spDim.group();
But are there any suggestions on how to define the scatter plot to handle multiple y values for each x value?
Here is a jsfiddle example showing how I can display the first element or the last element. But how can I show all elements of the array?
--- Additional Information ---
Above is just a simple example to demonstrate a requirement. We have developed a dynamic data explorer that is fully data driven. Currently the datasets being used are protected. We will be adding a public dataset soon to show off the various features. Below are a couple of images.
I have hidden some legends. For the Scatter Plot we added a vertical only brush that is enabled when pressing the "Selection" button. The notes section is populated on scatter plot chart initialization with the overall dataset statistics. Then when any filter is performed the notes section is updated with statistics of just the filtered data.
The field selection tree displays the metadata for the selected dataset. The user can decide which fields to show as charts and in datatables (not shown). Currently for the dataset shown we only have 89 available fields, but for another dataset there are 530 fields the user can mix and match.
I have not shown the various tabs below the charts DIV that hold several datatables with the actual data.
The metadata has several fields that are defined to help use dynamically build the explorer dashboard.
I warned you the code would not be pretty! You will probably be happier if you can flatten your data, but it's possible to make this work.
We can first aggregate all the injs within each uid, by filtering by the rows in the data and aggregating by uid. In the reduction we count the instances of each inj value:
uidDimension = ndx.dimension(function (d) {
return +d.uid;
}),
uidGroup = uidDimension.group().reduce(
function(p, v) { // add
v.actions.inj.forEach(function(i) {
p.inj[i] = (p.inj[i] || 0) + 1;
});
return p;
},
function(p, v) { // remove
v.actions.inj.forEach(function(i) {
p.inj[i] = p.inj[i] - 1;
if(!p.inj[i])
delete p.inj[i];
});
return p;
},
function() { // init
return {inj: {}};
}
);
uidDimension = ndx.dimension(function (d) {
return +d.uid;
}),
uidGroup = uidDimension.group().reduce(
function(p, v) { // add
v.actions.inj.forEach(function(i) {
p.inj[i] = (p.inj[i] || 0) + 1;
});
return p;
},
function(p, v) { // remove
v.actions.inj.forEach(function(i) {
p.inj[i] = p.inj[i] - 1;
if(!p.inj[i])
delete p.inj[i];
});
return p;
},
function() { // init
return {inj: {}};
}
);
Here we assume that there might be rows of data with the same uid and different inj arrays. This is more general than needed for your sample data: you could probably do something simpler if there is indeed only one row of data for each uid.
To flatten out the resulting group, with we can use a "fake group" to create one group-like {key, value} data item for each [uid, inj] pair:
function flatten_group(group, field) {
return {
all: function() {
var ret = [];
group.all().forEach(function(kv) {
Object.keys(kv.value[field]).forEach(function(i) {
ret.push({
key: [kv.key, +i],
value: kv.value[field][i]
});
})
});
return ret;
}
}
}
var uidinjGroup = flatten_group(uidGroup, 'inj');
Fork of your fiddle
In the fiddle, I've added a bar chart to demonstrate filtering by UID. Filtering on the bar chart works, but filtering on the scatter plot does not. If you need to filter on the scatter plot, that could probably be fixed, but it could only filter on the uid dimension because your data is too course to allow filtering by inj.
I was looking forward to sorting the bars in the bar chart (not stacked bar) using c3.js. But could not find any suitable way, there is an option mentioned below but that's not applicable for the bar chart.
data: {
order: 'asc'
}
In my case, all data are coming dynamically and rendering through c3.js to make a bar chart.I was looking for a sort like https://bl.ocks.org/mbostock/raw/3885705/
You were on the right track with your jsfiddle but as the data passed to c3.generate() is an array of datasets then you cannot just call data.sort().
EDIT
For this specific case, where your data is in the form you described in your comments, this would be a suitable method.
I primarily used array functions like slice, splice, map and sort. These are key functions to gain familiarity with if you are manipulating and plotting data. The mozzila docs are a great point to start.
You should also note which functions modify the array they are called on and which return a new array; Mutating your data when you did not intend to can often cause hard-to-spot bugs.
var data = [
["a", "b", "c"],
['data1', "30", " 200", " 100"]
]
// declare a function to control variable scope
var sortData = function(unsortedData) {
// deep copy array to avoid modification of input array
var sorted = unsortedData.map(function(row) {
// use slice to copy this array
return row.slice()
})
// remove the dataname
var name = sorted[1].splice(0, 1);
// produce an array of data points [[x1,y1],[x2,y2]...]
var datapoints = sorted[1].map(function(d, i) {
// use index in map function to pull out name
// return array for datapoint [x,y]
return [sorted[0][i], d];
});
//sort datapoints
var sortedData = datapoints.sort(function(a, b) {
return a[1] - b[1];
});
// map back into separate x and y data
sorted[1] = sortedData.map(function(point, i) {
// assign x value to data[0] element
sorted[0][i] = point[0];
// return the y data point
return point[1];
});
// add the dataname back into the y data
sorted[1] = name.concat(sorted[1]);
// add the 'x' label name to x-values
sorted[0].splice(0, 0, 'x')
// return the sorted array
return sorted
}
var chart = c3.generate({
data: {
x: 'x',
columns: sortData(data),
type: 'bar',
},
axis: {
x: {
type: 'category' // this needed to load string x value
}
}
})
<link href="https://cdnjs.cloudflare.com/ajax/libs/c3/0.4.18/c3.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/c3/0.4.18/c3.js"></script>
<div id="chart"></div>
I am trying to use my own data in a nvD3 stacked area chart. The sample data format from the Angular nvD3 site has a format like this:
[{
"key":"Series 1",
"values":[[1025409600000,0],[1028088000000,-6.3382185140371]]
},
{
"key":"Series 2",
"values":[[1025409600000,0],[1028088000000,0]]
}]
I have data coming from my database in this format:
[{
"Loc_Cnt":6,"Num_Cars":552,"Num_Employees":34,"active_month":"2017-10-01T00:00:00"
},
{
"Loc_Cnt":4,"Num_Cars":252,"Num_Employees":14,"active_month":"2017-11-01T00:00:00"
}]
I am trying to graph from my data, three series (Series 1: Flt_Cnt, Series 2: Num_Cars, Series 3: Num_Employees). For each series, the X axis value being the active_month date, and the Y axis value being the series value.
How can I either A) convert my data to look like the sample data easily, or B) use my data as is in the AngularJs nvd3 chart? I feel a .forEach on the array would not be efficient for larger data sets, and not as easy to read. I tried to use d3.nest in some way, but haven't been able to get a correct format. Thanks for your help!
It's not elegant, but I brute forced a way to my solution. If there are any better solutions, please do let me know.
var Loc_Cnt = [];
var Num_Cars = [];
var Num_Employees = [];
var obj = {};
//arr is the array of values in my format
arr.forEach(function (element) {
//need the date in milisecond format
var date = new Date(element.active_month);
var time = date.getTime();
//load corresponding arrays
Loc_Cnt.push([time, element.Loc_Cnt]);
Num_Cars.push([time, element.Num_Cars]);
Num_Employees.push([time, element.Num_Employees]);
});
//load each key/values pair into new object
obj["Loc_Cnt"] = Loc_Cnt;
obj["Num_Cars"] = Num_Cars;
obj["Num_Employees"] = Num_Employees;
//d3.entries creates an object of key/VALUEs
arrRollup = d3.entries(obj);
//change the key word values to value
var i;
for (i = 0; i < arrRollup.length; i++) {
arrRollup[i].values = arrRollup[i]['value'];
delete arrRollup[i].value;
}
I'm reading a csv file, and need to compute two figures from this data using D3.js or normal JavaScript:
This might be able to be done in one step, but I've broken it down for the purposes of explanation:
Once my data is read in, I need to iterate through each of the columns, labelled "one" to "ten"
(the length of this data is an unknown length, so it might go up to twelve or twenty),
...each time multiplying each column which comes after "multiplier" by variable called "multiplier"
(in the data, I gave it arbitrary values of 1.5, 1, 0,5 to make reading visually clearer).
This gives a new grid of figures from which a maximum score and minimum score of each of these new figures must be calculated for each ID from 1 to n. So each ID will have a max and minimum. I need to know the maximum and minimum of these new scores across the entire data returned as variables.
The data is read in:
d3.csv("data.csv", function(csv) {
var mydata = bars
.selectAll("rect")
.data(csv)
.enter()};
The example data appears as:
ID,total,mutiplier,one,two,three,four,five,six,seven,eight,nine,ten
1,16500,1.5,0.362,0.37,0.1,0.101,0.035,0.362,0.37,0.1,0.101,0.035
2,61000,1,0.426,0.382,0.115,0.084,0.053,0.426,0.382,0.115,0.084,0.053
3,48700,1.5,0.156,0.531,0.195,0.399,0.14,0.156,0.149,0.106,0.399,0.14
4,33000,0.5,0.462,0.409,0.149,0.106,0.149,0.106,0.085,0.1,0.106,0.051
5,8000,0.5,0.327,0.316,0.085,0.1,0.085,0.1,0.057,0.245,0.1,0.057
6,12760,1,0.149,0.195,0.057,0.245,0.057,0.245,0.119,0.114,0.245,0.08
This original data cannot be replaced as I reference it later.
So from this data, after iterating through all columns, and taking the max and min from each over the whole data --- the minimum is 0.003535 and the maximum is 3.8875575
...and I need the function to return a var min and var max for next calculation.
Hope someone out there can help!
One way you can load your data as a text, and latter use d3.csv.parseRows to parse your CSV as an array of arrays. So just take the slice, ignoring the first 3 columns.
d3.text('data.csv', function(text)
{
var rows = d3.csv.parseRows(text, function(row, index)
{
// skip header, coerce to Number values
if(index > 0)
{
return row.map(Number);
}
});
var extent = rows.reduce(function(result, row)
{
return d3.extent(result.concat(row.slice(3).map(function(value)
{
return value * row[2];
})));
}, [NaN, NaN]);
var min = extent[0];
var max = extent[1];
});
Other way if array of objects is more convenient structure for later plotting, you can do the following.
var nonMeasureColumns = {'ID': 0, 'total': 0, 'multiplier': 0};
d3.csv('data.csv')
.row(function(row)
{
for(var key in row)
{
row[key] = Number(row[key]);
}
return row;
})
.get(function(error, rows)
{
var extent = rows.reduce(function(result, row)
{
return d3.extent(result.concat(d3.map(row).entries()
.filter(function(entry)
{
return !(entry.key in nonMeasureColumns);
})
.map(function(entry)
{
return entry.value * row['multiplier'];
})
));
}, [NaN, NaN]);
var min = extent[0];
var max = extent[1];
});